Texture Shaders

Texture shaders evaluate a texture and typically return a color, scalar, or vector (but like any shader, return values can be freely chosen and may even be structures of multiple values). Textures can either be procedural, for example evaluating a 3D texture based on noise functions or calling other shaders, or they can do an image lookup. The .mi format provides different texture statements for these two types, one with a function call and one with a texture file name. Refer to texture section for details.

Texture shaders are not first-class shaders. This means that mental ray never calls one by itself and provides no special support for them. Texture shaders are called exclusively by other shaders. There are four ways of calling a texture shader from a material shader or other shaders:

  1. by requesting the value of a shader parameter using mi_eval, or
  2. by using a built-in convenience function like mi_lookup_color_texture, or
  3. by calling the shader function directly if the C function name is known, or
  4. with a statement like
    mi_call_shader_x(result, miSHADER_TEXTURE, state, tag, args);
    
    The tag argument references the texture function. The texture function is a data structure in the scene database that contains a reference to the C function itself, plus a pointer to a user argument block arg that is passed to the texture shader when it is called. Such user arguments are rarely used, and mi_call_shader_x is the only way to pass them to a shader. The caller would have to write the required arguments into this user argument structure args; it would not have access to shader parameters specified in the .mi file.

The recommended way of calling subshaders is implicitly with mi_eval, which does not require the calling shader to be aware that a subshader provides the value. This makes shaders very flexible by allowing them to be combined in arbitrary ways from the scene file, without changing and recompiling shader sources. However, there are cases when not the value of a shader but a shader itself is passed as a parameter, and mi_call_shader_x provides a good way of calling such shaders.

Unlike material shaders, texture shaders usually return a simple color or scalar or other return value. There are no lighting calculations or secondary rays. This greatly simplifies the task of changing a textured surface. For example, a simple texture shader that does a simple, non-antialiased lookup in a texture image could be written as:

struct mib_texture_lookup {
    miTag           tex;
    miVector        coord;
};

DLLEXPORT miBoolean mib_texture_lookup(
    miColor         *result,
    miState         *state,
    struct mib_texture_lookup *paras)
{
    miTag           tex   = *mi_eval_tag(&paras->tex);
    miVector        *coord = mi_eval_vector(&paras->coord);

    if (tex && coord->x >= 0 && coord->x < 1
            && coord->y >= 0 && coord->y < 1
            && mi_lookup_color_texture(result, state, tex, coord))

            return(miTRUE);

    result->r = result->g = result->b = result->a = 0;
    return(miFALSE);
}

This shader can be assigned to parameters of another shader, such as a material shader, causing that shader to see textured color inputs without having to implement texturing itself. Here is an example scene fragment:

color texture "texfile" "/some/directory/imagefile.rgb"

shader "coordshader" "mib_texture_vector" (...)

shader "texshader" "mib_texture_lookup" (
    "coord" = "coordshader"
    "tex"   "texfile")

material "mtl"
    "mib_illum_phong" (
        "ambient" = "texshader",
        "diffuse" = "texshader",
        ...)
end material

Note that texshader is defined here as a named shader because it is going to be assigned in the material. Instead of using a file texture for the tex parameter, another shader, perhaps a procedural marble shader, could have been used. The texture shader needs to know which point on the texture to look up, as a vector assigned to the coord parameter. Coordinate lookups are a very flexible way to implement all sorts of projections, wrapping, scaling, replication, distortion, cropping, and many other functions, so this is also implemented as another shader. It could be done inside the texture lookup shader itself, but separating it out into a separate shader allows all those projections and other manipulations to be implemented only once, instead of in every texture shader. Here is an example of a texture projection shader like mib_texture_vector used in the example above (in a simplified form):

#define TP_NONE     0       /* texture/bump-basis vector projection */
#define TP_XY       2
#define TP_XZ       3
#define TP_YZ       4
#define TP_SPHERE   5

#define TS_INTERNAL 0       /* texture coordinate spaces (selspace) */
#define TS_OBJECT   1
#define TS_WORLD    2
#define TS_CAMERA   3

#define EPS         1e-4

struct uv {miVector u, v;};

struct mib_texture_vector_simple {
    miInteger       selspace;
    miInteger       project;
};

DLLEXPORT miBoolean mib_texture_vector_simple(
    miVector        *result,
    miState         *state,
    struct mib_texture_vector_simple *paras)
{
    miVector        v;              /* working vector */
    miVector        *vp;            /* pointer to working vector */
    miVector        tmp, *tri[3];   /* temporaries */
    miInteger       selspace = *mi_eval_integer(&paras->selspace);
    miInteger       project;
    miBoolean       success;        /* is mapping successful */

    project = *mi_eval_integer(&paras->project);
    vp = &state->point;

    if (selspace && state->time != 0) {
            v.x = vp->x + state->motion.x * state->time;
            v.y = vp->y + state->motion.y * state->time;
            v.z = vp->z + state->motion.z * state->time;
            vp = &v;
    }
    switch(selspace) {
      case TS_OBJECT:
            mi_point_to_object(state, &v, vp);
            vp = &v;
            break;

      case TS_WORLD:
            mi_point_to_world(state, &v, vp);
            vp = &v;
            break;

      case TS_CAMERA:
            mi_point_to_camera(state, &v, vp);
            vp = &v;
            break;
    }
    success = miTRUE;
    switch(project) {
      case TP_XY:
            v.x = vp->x; v.y = vp->y; v.z = 0;
            vp = &v;
            break;

      case TP_XZ:
            v.x = vp->x; v.y = vp->z; v.z = 0;
            vp = &v;
            break;

      case TP_YZ:
            v.x = vp->y; v.y = vp->z; v.z = 0;
            vp = &v;
            break;

      case TP_SPHERE: {
            float length;
            length = mi_vector_norm(vp);
            /* put seam in x direction */
            if (!vp->z && !vp->x) {
                    /* singularity point */
                    v.x = 0.0;
                    success = miFALSE;
            } else
                    v.x = atan2(-vp->z, vp->x) / M_PI / 2;

            if (v.x < 0) v.x += 1;
            /* y comes from asin */
            if (length)
                    v.y = asin(vp->y / length) / M_PI + 0.5;
            else
                    v.y = 0.5f;
            v.z = 0;
            vp = &v;
            break; }
    }
    *result = *vp;
    state->tex = *result;
    return(success);
}

This shader only picks up a vector, here from state→point. The complete base shader has more methods for picking up vectors, especially from state→tex, a variable that is commonly used to pass texture lookup coordinates to a texture shader. The mi_lookup_color_texture function does this, for example.

Remapping, including scaling, rotation, cropping, replication, etc., is handled by yet another shader (mib_texture_remap) from the base shader library. This keeps the base shaders as flexible as possible, although in practice higher performance can be achieved by writing shaders that perform coordinate lookup and remapping within a single shader.

Finally, here is a shader that uses full elliptical filtering for better texture anti-aliasing, especially of textures seen at a sharp angle:

struct mib_texture_filter_lookup {
    miTag           tex;
    miVector        coord;
    miScalar        eccmax;
    miScalar        maxminor;
    miScalar        disc_r;
    miBoolean       bilinear;
    miUint          space;
    miTag           remap;
};

#define DISC_R      0.3     /* projection matrix circle radius */
#define CIRCLE_R    0.8     /* projected screen-space circle */

DLLEXPORT miBoolean mib_texture_filter_lookup(
    miColor         *result,
    miState         *state,
    struct mib_texture_filter_lookup *paras)
{
    miTag           tex = *mi_eval_tag(&paras->tex);
    miVector        *coord;
    miUint          space;
    miTag           remap;
    miVector        p[3], t[3];
    miMatrix        ST;
    miTexfilter     ell_opt;
    miScalar        disc_r;

    if (!tex) {
            result->r = result->g = result->b = result->a = 0;
            return(miFALSE);
    }
    coord  = mi_eval_vector(&paras->coord);
    space  = *mi_eval_integer(&paras->space);
    disc_r = *mi_eval_scalar(&paras->disc_r);
    if (disc_r <= 0)
            disc_r = DISC_R;
    if (state->reflection_level == 0 &&
        mi_texture_filter_project(p, t, state, disc_r, space) &&
        (remap = *mi_eval_tag(&paras->remap))) {
            mi_call_shader_x((miColor*)&t[0], miSHADER_TEXTURE,
                                            state, remap, &t[0]);
            mi_call_shader_x((miColor*)&t[1], miSHADER_TEXTURE,
                                            state, remap, &t[1]);
            mi_call_shader_x((miColor*)&t[2], miSHADER_TEXTURE,
                                            state, remap, &t[2]);
            if (mi_texture_filter_transform(ST, p, t)) {
                    ell_opt.eccmax    = *mi_eval_scalar(&paras->eccmax);
                    ell_opt.max_minor = *mi_eval_scalar(&paras->maxminor);
                    ell_opt.bilinear  = *mi_eval_boolean(&paras->bilinear);
                    ell_opt.circle_radius = CIRCLE_R;
                    ST[2*4+0] = coord->x;
                    ST[2*4+1] = coord->y;
                    if (mi_lookup_filter_color_texture(result, state,
                                                    tex, &ell_opt, ST))
                            return(miTRUE);
            }
    }
    /* fallback to standard pyramid or nonfiltered texture lookup */
    return(mi_lookup_color_texture(result, state, tex, coord));
}

This shader is based on calling a texture shader multiple times to derive gradients that can be passed to mi_texture_filter_transform, and otherwise passing shader parameters to mi_lookup_filter_color_texture, which handles the elliptical lookup. For more information on elliptical texture lookups, refer to the base shader documentation.

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