There are 15 kinds of shaders that you can write:

If you're like me, you should be itching for some action by now, so let's have a go at it:

Type the following code into a file called customs.c

#include "shader.h"

struct plaincolor {
  miColor color;
  miScalar brightness;
};

DLLEXPORT int plaincolor_version(void) {return 1;}

DLLEXPORT miBoolean plaincolor(
  miColor *result,
  miState *state,
  struct plaincolor *paras
)
{
  miColor *color = mi_eval_color(&paras->color);
  miScalar *brightness = mi_eval_scalar(&paras->brightness);

  result->r = color->r * (*brightness);
  result->g = color->g * (*brightness);
  result->b = color->b * (*brightness);

  return miTRUE;
}

and compile it. Place the resulting customs.dll file together with the scene file shadertest.mi.

Create a text file called customs.mi with the following content:

declare shader
  color "plaincolor" (
    color "color",
    scalar "brightness"
  )
  version 1
end declare

Then edit shadertest.mi to insert the following statements after the fourth line

link "customs.dll"
$include "./customs.mi"

material "constmat"
  "plaincolor" ("color" 1 0.7 0, "brightness" 0.9)
end material

and replace the object rightcover's shader with "constmat". That's it! Do a render and see the effect of your first shader.

That code above shows the basic form of any MR shader:

  1. Include the shader.h header file.

  2. List out the parameters of the shader in a structure. The types and names of these parameters must match those of the parameters in the shader declaration in the MI file later.

  3. Set up a simple function to return a version number. This number should match the version number near the end of the shader declaration in the MI file. You must name this function with the main shader function's name appended with "_version"

  4. The main shader function itself should return a miBoolean (enum with miTRUE or miFALSE value) to indicate the success or failure of the shader calculation. This function receives three arguments from the renderer -- a pointer to the expected result, a pointer to the data provided by the scene (called the state), and a pointer to the parameter structure. Put simply, writing a shader is usually just a matter of playing with state variables and shader parameters, and then putting what you come up with in the result.

  5. You might be wondering what those strange-looking types like miColor and miScalar are. The answers are in shader.h. Go on, use your favourite text editor (I hope it's not Notepad) to find out.

  6. The mi_eval_* functions take a pointer to one of the parameters and return that parameter's value. We don't access the parameters directly because they might be connected to other shaders. These functions will take care of calling the connected shaders and collecting their results.

Note that you must precede both the version function and the shader function with DLLEXPORT. This is a Windows quirk.


Procedural texture

Let's make a funky texture with that "function that launched a thousand textures" (aka mother of funky textures), the noise function.

Open the customs.c source file and append the following code to it:

struct splotchy {
  miColor color1;
  miColor color2;
  miScalar spread;
};

DLLEXPORT int splotchy_version(void) {return 1;}

DLLEXPORT miBoolean splotchy(
  miColor *result,
  miState *state,
  struct splotchy *paras
)
{
  miScalar cindex;

  mi_vector_div(&state->point, paras->spread);
  cindex = mi_noise_3d(&state->point);

  if (cindex < 0.5) {
    result->r = paras->color1.r;
    result->g = paras->color1.g;
    result->b = paras->color1.b;
  }
  else {
    result->r = paras->color2.r;
    result->g = paras->color2.g;
    result->b = paras->color2.b;
  }

  return miTRUE;
}

If you've had even a cusory knowledge about the noise function, you should know that you can visualize it like a hilly terrain. This shader just says where you hit those "hills" lower than a certain height (0.5 actually), you get one color, and where you hit them higher, you get another color.

mi_vector_div() takes a pointer to a vector and divides that vector by a scalar. This function is convenient because otherwise you cannot apply arithmetic operation directly to a vector and a scalar.

mi_noise_3d() takes a pointer to a point location, uses it to sample a noise table, and returns a value between 0 and 1.

Now let's put this beauty into action. First, append

declare shader
  color "splotchy" (
    color "color1",
    color "color2",
    scalar "spread"
  )
  version 1
end declare

to customs.mi. Then insert

shader "splotchtexture"
  "splotchy" (
    "color1" 0.83 0.72 0.57,
    "color2" 0.2 0.32 0.7,
    "spread" 0.5
  )

material "wallmat"
  "mib_illum_lambert" (
    "diffuse" = "splotchtexture",
    "lights" ["light1"]
  )
end material

into shadertest.mi, just before the wall object definition. Finally, assign "wallmat" to the wall object.

Render the scene.

See? A color shader can be either a material or a texture, depending on where you call it.


Displacement

struct wormbulge {
  miScalar amplitude;
  miScalar frequency;
};

DLLEXPORT int wormbulge_version(void) {return 1;}

DLLEXPORT miBoolean wormbulge(
  miScalar *result,
  miState *state,
  struct wormbulge *paras
)
{
  *result = fabs(sin(state->tex_list[0].y * paras->frequency * M_PI_2)) * paras->amplitude;
  return miTRUE;
}

Remember to include math.h for the fabs and sin functions.

tex_list is an array of vectors representing the tex co-ords in the texture spaces attached to the hit surface. M_PI_2 is a symbolic constant defined in shader.h representing half the value of pi. sin of course takes an angle in radians and returns a sine value. fabs returns the absolute value of its argument.

To use this shader,

  1. add the necessary declaration to customs.mi.

  2. Open shadertest.mi and insert

    material "cucumbermat"
      "mib_illum_phong" (
        "diffuse" 0.35 0.53 0.9,
        "specular" 0.6 0.6 0.6,
        "exponent" 50,
        "mode" 0,
        "lights" ["light1"]
      )
      displace "wormbulge" ("amplitude" 0.05, "frequency" 24)
    end material
    before the cucumber object definition. Remember displace expects a scalar-returning shader after it.

  3. In the cucumber object definition, insert

    max displace 0.05
    just before the basis specifications.

  4. Assign "cucumbermat" to cucumber.

  5. Insert

    approximate displace fine sharp 1 view length 1 "mainsurface"
    just before the end of the group block in the cucumber definition. This statement implements an especially fine tessellation for the displacement. It keeps creases in the displacement pattern sharp (sharp 1), and divides the surface such that each sub-triangle edge is not more than 1 pixel in projected length (view length 1).

Render and have a look.

Remember I told you displacement shaders can move surface points anywhere, not just along the surface normals? Let's see how that works now: we're going to turn the ball in the scene into a cube.

struct cubify {miScalar width;};

DLLEXPORT int cubify_version(void) {return 1;}

DLLEXPORT miBoolean cubify(
  miScalar *result,
  miState *state,
  struct cubify *paras
)
{
  miScalar halfwidth, xval, yval, zval, factor;
  /* Projection plane's position and normal */
  miVector orig, norm;

  halfwidth = paras->width / 2;

  xval = fabs(state->point.x);
  yval = fabs(state->point.y);
  zval = fabs(state->point.z);

  if (xval >= yval && xval >= zval) {
    norm.x = 1; norm.y = norm.z = 0;
    orig.x = state->point.x > 0 ? halfwidth : -halfwidth;
    orig.y = orig.z = 0;
  }
  else if (yval >= xval && yval >= zval) {
    norm.y = 1; norm.x = norm.z = 0;
    orig.y = state->point.y > 0 ? halfwidth : -halfwidth;
    orig.x = orig.z = 0;
  }
  else {
    norm.z = 1; norm.x = norm.y = 0;
    orig.z = state->point.z > 0 ? halfwidth : -halfwidth;
    orig.x = orig.y = 0;
  }

  factor = mi_vector_dot(&orig,&norm) / mi_vector_dot(&state->point,&norm);
  mi_vector_mul(&state->point, factor);

  return miTRUE;
}

Create a material for the ball and apply this displacement shader to it as usual. Set the "width" parameter to 3.4, which is twice the ball's radius. Set a max displace of 1.245 for the ball object -- this is roughly the maximum displacement distance between the original ball surface and the corner of the final cube. And lastly, insert a

approximate displace fine sharp 1 view length 0.2 "mainsurface"

near the end of the group block in the ball object definition; this time we have to set a shorter length for the subdivided edges to preserve the sharpness in the cube's edges.

So, given the power to move any and every point on a sphere, how do you transform the sphere into a cube?

As shown in the illustration on the left, first we need to find out the closest cube face to project each point on to. Then, given the point position P and a plane facing N and situated at O, the destination point on the plane to transform the sphere point to will be P*((O.N)/(P.N)).

It's fairly simple to find out the closest projection plane -- you just have to check which is the longest component in the point co-ordinates, then the plane will just face that component direction.

mi_vector_dot() calculates the dot product between the vectors pointed to by its arguments. mi_vector_mul() multiplies the vector in its first argument by its second scalar argument.


Reflection

Fuzzy reflection, to be precise... did I just say something contradictory?

struct blureflect {miScalar shiny;};

DLLEXPORT int blureflect_version(void) {return 1;}

DLLEXPORT miBoolean blureflect(
  miColor *result,
  miState *state,
  struct blureflect *paras
)
{
  miVector rdir;
  miColor rcol;
  miScalar atten;

  mi_reflection_dir_glossy(&rdir, state, paras->shiny);
  if( mi_trace_reflection(&rcol, state, &rdir) ) {
    mi_trace_probe(state, &rdir, &state->point);
    atten = 1 - state->child->dist / 10;
    if(atten < 0) atten = 0;
    result->r = rcol.r * atten;
    result->g = rcol.g * atten;
    result->b = rcol.b * atten;
  }

  return miTRUE;
}

The mi_reflection_dir_glossy function takes the state information in its second argument, computes a reflection direction and puts it in its first (pointer) argument. This reflection direction isn't a single vector like the one you'd get from mi_reflection_dir(); in each sampling it's randomized within the constraint of the function's third argument, which works like the specular exponent in the familiar Phong shading function -- the greater it is, the less spread-out the resulting direction would be (useful range 20 - 100).

mi_trace_reflection() takes a pointer to a direction in its third argument, shoots a reflection ray in that direction and returns a hit color in its first argument. The function itself returns miTRUE if something is hit, or miFALSE otherwise. Every invocation of this function consumes one level of reflection.

mi_trace_probe() starts out from the position pointed to by its third argument, travels along the direction pointed to by its second argument, and checks whether it can hit a surface -- if it can, it returns miTRUE (or miFALSE otherwise) and stuffs the state information at the hit point into state->child. Within these information, dist contains the length of the probe ray. We use this variable to dim the reflection of surface points that are far away from the reflecting surface -- consider the attenuating factor atten, which ranges from 1 at points next to the reflecting surface to 0 at points that are 10 or more units away from the reflection point.

This shader computes only reflections. There's no direct illumination calculation at all. But there's no need to write a special direct illumination function here because there're already a couple of good ones in the standard shader library. You just need to combine one of them with your shader, with some help from a little shader:

struct simplemixer {
  miColor input1;
  miColor input2;
  miScalar factor1;
  miScalar factor2;
};

DLLEXPORT int simplemixer_version(void) {return 1;}

DLLEXPORT miBoolean simplemixer(
  miColor *result,
  miState *state,
  struct simplemixer *paras
)
{
  miColor *col1 = mi_eval_color(&paras->input1);
  miColor *col2 = mi_eval_color(&paras->input2);
  miScalar *fac1 = mi_eval_scalar(&paras->factor1);
  miScalar *fac2 = mi_eval_scalar(&paras->factor2);

  result->r = col1->r * (*fac1) + col2->r * (*fac2);
  result->g = col1->g * (*fac1) + col2->g * (*fac2);
  result->b = col1->b * (*fac1) + col2->b * (*fac2);

  return miTRUE;
}

This shader simply takes two input colors, multiplies them by a couple of factors, and then adds them together.

Now let's put the shaders to action: append the parameter declarations of blureflect and simplemixer to customs.mi as usual, and then insert the following statements to shadertest.mi (just before the ground object definition)

shader "groundcolor" "mib_illum_lambert" ("diffuse" 0.45 0.5 0.85, "lights" ["light1"])

shader "reflectcolor" "blureflect" ("shiny" 20)

material "groundmat"
  "simplemixer" ("input1" = "groundcolor", "input2" = "reflectcolor", "factor1" 1, "factor2" 0.4)
end material

and render.


Wood

For some reason, the standard library doesn't have a wood texture. A major oversight, if you asked me. But we can write one ourselves, can't we?

DLLEXPORT int wood_version(void) {return 1;}

#define numcentrs 20

static miVector centrs[numcentrs];

DLLEXPORT void wood_init(
  miState *state,
  void *paras,
  miBoolean *inst_req
)
{
  int i;

  mi_srandom(42239);

  for (i = 0; i < numcentrs; i++) {
    centrs[i].x = mi_random() * 2;
    centrs[i].y = mi_random() / 5;
    centrs[i].z = 0;
  }
}

First, we set up an initialization shader to plant the centers of wood knots. An initialization shader has the same name as the action shader, but with "_init" appended. It's run once in each rendering session (instead of once for every ray) just before the main shader begins its action, so you should normally use it to set up unchanging things like, well, wood knots. An init shader takes as arguments a state pointer, a parameter pointer and a pointer to a boolean variable that specifies whether to initialize for each shader instance. Set the value pointed to by this last argument to miTRUE in the function body if you want instance initialization, in which case you'll get a well-defined paras; if you leave this variable alone, like we do here, paras will be null.

You can also write an exit shader (shader name ends with "_exit") to do the cleanup chores after the rendering, but we are not going to write one here.

The knot positions are stored as a static array of vectors so that other shader functions can read them.

The mi_srandom function seeds a random number sequence. By giving the same integer as its argument and calling mi_random() the same number of times subsequently, you'll get the same sequence of random numbers. mi_random() returns a random scalar between 0 and 1. We expand it in the x direction and compress it in the y direction in order to match the stretching of the wood pattern in the main shader later.

And the wood pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
miScalar getpit(miVector sampos)
{
  miVector gradient;
  miScalar pits;

  pits = mi_noise_3d_grad(&sampos, &gradient);
  if (pits > 0.37) pits = 0.37;
  pits *= 2.702;    // push 0.37 up to 1

  return pits;
}

struct wood {
  miColor color1;
  miColor color2;
};

DLLEXPORT void wood(
  miColor *result,
  miState *state,
  struct wood *paras
)
{
  int i;
  miVector scalvec, dvec, sampos;
  miScalar d, indx, holes, sum = 0;

  // main pattern
  scalvec.x = state->tex_list[0].x * 2;
  scalvec.y = state->tex_list[0].y / 5 + mi_noise_1d(state->tex_list[0].x * 260) / 150;
  scalvec.z = 0;

  for (i = 0; i < numcentrs; i++) {
    mi_vector_sub(&dvec, &scalvec, &centrs[i]);
    d = mi_vector_norm(&dvec);
    sum += 22 / exp(11 * d);
  }

  indx = fabs(mi_noise_1d(sum) - 0.5) + 0.5;

  result->r = (1 - indx) * paras->color1.r + indx * paras->color2.r;
  result->g = (1 - indx) * paras->color1.g + indx * paras->color2.g;
  result->b = (1 - indx) * paras->color1.b + indx * paras->color2.b;

  // pits
  sampos.x = state->tex_list[0].x * 1400;
  sampos.y = state->tex_list[0].y * 25;
  sampos.z = 34.3;

  holes = getpit(sampos);

  result->r *= holes;
  result->g *= holes;
  result->b *= holes;
}

We use the knot points as centers of value fields with exponential fall-off (left picture above). These fields are summed (lines 33-37) to create a height image that varies smoothly (both pictures, can you see their correspondence?).

The values in this image are used to lookup a 1D noise (line 39). Again take note of the correspondence between this picture and the ones above. The resulting noise is folded up to create sharper edges.

Actually we've taken care to stretch the texture domain (lines 29 and 30, recognize the stretching factors?)...

and perturb the vertical tex co-ord a bit (line 30).

The greyscale values are then used to mix the two input wood colors (lines 41 to 43).

Lines 1-11 define a function to cover the whole texture surface with grain pits. The mi_noise_3d_grad function returns a 3D gradient noise. Line 7 chops off the top of the noise terrain, and line 8 raises the chopped terrain back to a unit height.

Finally, lines 52-54 put the pits on the wood pattern.

Notice we don't return anything from the main shader this time, and we don't put DLLEXPORT in front of the local function getpit().

Now, to use the shader, add its paramater declaration to customs.mi as usual, and then insert the following lines just before the definition of the object "leftcover" in shadertest.mi:

shader "woodcol" "wood" ("color1" 0.45 0.338 0.284, "color2" 0.9 0.724 0.568)

material "woodmat"
  "mib_illum_phong" (
    "diffuse" = "woodcol",
    "specular" 0.3 0.3 0.3,
    "exponent" 140,
    "mode" 0,
    "lights" ["light1"]
  )
end material

And finally, assign "woodmat" to the "leftcover" object (not instance).


Panoramic lens

Let's make a lens that takes a 360 view of its surroundings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct panoramic {miScalar height;};

DLLEXPORT int panoramic_version(void) {return 1;}

DLLEXPORT miBoolean panoramic(
  miColor *result,
  miState *state,
  struct panoramic *paras
)
{
  miScalar uval, vval, rayx, rayy, rayz;
  miVector raydir, rayorig, rayorig_t, raydir_t;

  uval = 2 * M_PI * state->raster_x / state->camera->x_resolution;
  vval = state->raster_y / state->camera->y_resolution;

  rayx = -sin(uval);
  rayy = (vval - 0.5) * paras->height;
  rayz = cos(uval);

  raydir.x = rayx; raydir.y = rayy; raydir.z = rayz;
  mi_vector_from_camera(state, &raydir_t, &raydir);
  rayorig.x = 0; rayorig.y = 0; rayorig.z = 0;
  mi_point_from_camera(state, &rayorig_t, &rayorig);

  return (mi_trace_eye(result, state, &rayorig_t, &raydir_t));
}

What we want to do here is sweep the viewing rays all around the camera.

The pictures above illustrate code lines 14 and 15, which compute the cylindrical co-ordinates of the current rendering sample around the camera in 3D space. raster_x and raster_y are the 2D raster co-ordinates of the rendering sample. Lines 17-18 then convert the cylindrical co-ordinates to a projection vector pointing out from the camera position.

The whole point about this lens shader business is to cast a seminal viewing ray using mi_trace_eye(). And since this function requires its last two arguments to be in internal space, we make sure it gets what it wants with lines 22 and 24.

Now, to use the shader, declare its parameter in customs.mi, turn off scanline rendering in the shadertest.mi option block (a necessary step for any lens shader to change the viewing rays), and add the line

lens "panoramic" ("height" 5)

just after the output statement in the camera definition. At this point you should also comment out the displace shader in "ballmat" because the ball's displacement looks really crude in panoramic close-up.