[welcome] [download] [documentation] [gallery] [how to contribute] [writing shaders] [contributors]

 

Writing a Shader
(Using the Shades routines to convert a RenderMan shader to a Lightwave3D plug-in)

Introduction

The original plugins in the Shades package (Planks2D, Parquet2D, and Fire2D) were converted from freely available RenderMan shaders. Not all the plugins in Shades need to originate as RenderMan shaders, but since there is a fair amount of information available about writing RenderMan shaders, it's a nice place to begin. If you are looking for some interesting shaders to study, the best place to start is at the Renderman Repository (RMR). The Surface Shader Section will give you plenty to choose from. You may also be interested in the RManNotes, by Steve May, which includes basic information about writing shaders, and generating regular and stochastic patterns. For general information about procedural texturing, not necessarily related to RenderMan, try the Noise, Turbulence, and Texture paper by Dr. Matthew Ward, and the Cellular Texture Basis Functions pages by Steve Worley.

Many of the Shades routines are intended to aid in the translation of RenderMan shaders to Lightwave. There are quite a few things you need to code into a Lightwave shader that do not exist for a RenderMan shader. Most of these things deal with the user interface, and loading and saving your shader's instance parameters into its assigned Lightwave object file. But these tasks can all be simplified by using the appropriate Shades' functions, which include support for:

  • a lwpanels interface for setting shader parameters.
  • common Lightwave texture parameters.
  • envelope access to all float and 3-vector parameters.
Many math and geometric functions are defined by the RenderMan Shading Language that are not found in the standard C math libraries. So Shades includes many math and texture mapping routines that the Lightwave plugin writer can use. There are other RenderMan Shading Language features that still need to be supported, and hopefully some of those will be added soon (pages references are to "The RenderMan Companion: A Programmer's Guide to Realistic Computer Graphics", by Steve Upstill):
  • more global variables. (page 296)
  • geometric functions. (page 325)
  • derivative functions. (page 313)

The easiest way to become acquainted with the existing routines, is to add a shader to the package by modifying an existing RenderMan shader. The following tutorial is designed to guide you through creating and adding a plugin to the Shades collection. The tutorial will lead you through the translation of the Fire.sl RenderMan shader. It requires that you have the Shades source code distribution, and a compiler for your platform. Currently I have specific distributions for SGI and Intel (Visual C++) platforms. Since the example uses the planks2d.c and planks2d.h files as the basis for modifications, you may also be interested in the LGPlanks.sl shader since it was the original RenderMan shader used to create those files.

I do not intend for this tutorial to be an introduction to Lightwave plugin programming. Refer to the current LWSDK documentation if you want to know more about the shader's server functions and detailed use of the envelopes and panels global functions. Visit the "Official Lightwave 3D Developers Page" for more developer information and software. Ernie Wright has some great pages about Lightwave programming and compiling plugins. Another site that will be beneficial to new plug-in programmers is Brenden Mecleary's step-by-step tutorial about writing a Lightwave Image Filter plug-in. (SORRY, it looks like Brenden's web pages are gone.)

This tutorial will not teach you the principals of writing a shader. If the links to the on-line information listed above are not enough, there are several books that discuss the RenderMan shading language. "The RenderMan Companion: A Programmer's Guide to Realistic Computer Graphics", by Steve Upstill is the original book on RenderMan in general (not just shaders). "Texturing and Modeling: A Procedural Approach", by Ebert, Musgrave, Peachey, Perlin, and Worley is an excellent textbook on the subject of procedural shader writing.

Creating your own shader

Instead of writing a shader from scratch, the easiest approach is to modify an existing shader. If you are familiar with the shaders currently in the Shades package, you should choose one that includes parameters similar to the ones needed in your shader (floats, colors, and/or 3-vectors). With the use of this document and the MODIFY/ENDMODIFY comments found in each of the shader's source files, hopefully the successful translation of a RenderMan shader to Lightwave will be possible.

Copy an existing shader.
Copy the .c and .h file of an existing shader to new filenames for your shader.

copy planks2d.c fire2d.c
copy planks2d.h fire2d.h).


Preparing to edit the new files.
 
When editing these new files, you will definitely be replacing all the code between a MODIFY comment and its corresponding ENDMODIFY comment. This is code that is specific to each individual shader, but the existing code should provide an example for your changes.

The remaining code implements the common texture parameters and the overall control of the plugin and its interface. You may also want to change this code for various reasons. For instance, if you are writing a 3D shader rather than the existing 2D shaders, the common texture parameter interface does not need the Texture Mapping Type pull-down menu or the Axis selection buttons. Another example might be that your shader makes no attempt at Anti-aliasing, so you have decided it would look best to unselect and ghost out the Anti-aliasing button (or maybe just remove it all together).


Edit the fire2d.h file.
 
  1. Search for the first MODIFY comment. This first MODIFY describes doing a global replace throughout the file. All other MODIFY comments will describe changes that will only take place between it and its corresponding ENDMODIFY comment.

  2. The second (and last) MODIFY comment in the fire2d.h is where you will declare all of the parameters you want for your shader. By choosing an existing parameter that is the same type as one of yours, just replace the existing variable name with your own. (IMPORTANT: As it says in the comments, if your parameter is a float, use the EnvData type instead. If it is a color or a 3-vector, use the EnvData3 type.)

    Here are the declarations used in the Fire2D instance:

    /* a float used to control the "speed" of the animated fire */
    EnvData timefactor;

    /* the 4 colors used to define the fire, from top to bottom */
    EnvData3 topcolor;
    EnvData3 midtopcolor;
    EnvData3 midlowcolor;
    EnvData3 lowcolor;


    Save these changes, and prepare to edit the new shader file.


Edit the fire2d.c file (a step for each MODIFY/ENDMODIFY block).
 
  1. The first MODIFY comment describes the same global string replace as in the .h file.

  2. Change the name of the include file to your shader's .h file.

    #include "fire2d.h"


  3. If this is your first attempt at writing a shader, TVERSION can be any integer you want. It will only become important when you are loading and saving objects that used a different version of this plugin.

  4. (Create_Fire2D function) This is another optional MODIFY where the common texture parameters are being initialized. Since the Fire shader does not support any internal anti-aliasing, the boolean for that parameter should be off. All other statements in this section remain as is.

    inst->texture.AntiAlias = FALSE;


  5. (Create_Fire2D function) Initialize the parameters for your shader. All of the parameters declared in the Fire2D instance must be initialized to their default values.

    init_EnvData (&inst->timefactor, NULL, 1.0);
    init_EnvData3 (&inst->topcolor, NULL, 1.0, 0.3, 0.1);
    init_EnvData3 (&inst->midtopcolor, NULL, 0.95, 0.7, 0.05);
    init_EnvData3 (&inst->midlowcolor, NULL, 0.95, 0.95, 0.1);
    init_EnvData3 (&inst->lowcolor, NULL, 1.0, 1.0, 0.8);


  6. (Destroy_Fire2D function) Each of your parameter's envelopes must be destroyed here, so the memory for the entire instance can be freed.

    if (inst->timefactor.env) (*envHand->destroy) (inst->timefactor.env);
    if (inst->topcolor.env) (*envHand->destroy) (inst->topcolor.env);
    if (inst->midtopcolor.env) (*envHand->destroy) (inst->midtopcolor.env);
    if (inst->midlowcolor.env) (*envHand->destroy) (inst->midlowcolor.env);
    if (inst->lowcolor.env) (*envHand->destroy) (inst->lowcolor.env);


  7. (DescLn_Fire2D function) These are local variables needed for evaluating the EnvData3 parameters shown in the next step. If you need to evaluate an EnvData parameter, just declare the variable to be a double.

    double c1[3], c2[3];


  8. (DescLn_Fire2D function) The top and bottom colors are used to describe this plug-in instance. The color is evaluated at time 0, if there is an envelope assigned to it.

    eval_EnvData3 (&inst->topcolor, 0, c1);
    eval_EnvData3 (&inst->lowcolor, 0, c2);
    sprintf (inst->desc, "ML_Fire2D: [%3d %3d %3d] -> [%3d %3d %3d]", (int)(c1[0] * 255.0), ...);


  9. (Save_Fire2D function) All your EnvData and EnvData3 parameters will be saved to the object file after the common texture parameters have been saved.

    write_EnvData (sState, &inst->timefactor);
    write_EnvData3 (sState, &inst->topcolor);
    write_EnvData3 (sState, &inst->midtopcolor);
    write_EnvData3 (sState, &inst->midlowcolor);
    write_EnvData3 (sState, &inst->lowcolor);


  10. (Load_Fire2D function) Read the parameters from an object file in the same order they were saved in the previous step.

    read_EnvData (lState, &inst->timefactor);
    read_EnvData3 (lState, &inst->topcolor);
    read_EnvData3 (lState, &inst->midtopcolor);
    read_EnvData3 (lState, &inst->midlowcolor);
    read_EnvData3 (lState, &inst->lowcolor);


  11. (NewTime_Fire2D function) The Fire2D shader does not do any pre-calculations, so the statements between this MODIFY and ENDMODIFY pair can be deleted. However, if your shader code uses values that only need to be calculated once at each time step, this is the function for the pre-calculations. So, any local variables used in the next step will need to be declared here.

  12. (NewTime_Fire2D function) Since the Fire2D shader doesn't do any pre-calculations, this block is also deleted. However, notice the pre-calculated values in the Planks2D code are assigned to double variables declared in the Planks2D instance. Those values are kept with the instance until the next time step, when this function is called again.

  13. (Flags_Fire2D function) The Fire2D shader changes the color and transparency of the surface. The changes to the surface values are actually made at the very end of the Evaluate_Fire2D function (see below).

    return (LWSHF_COLOR || LWSHF_TRANSP);


  14. (Evaluate_Fire2D function) It will be helpful to view the Fire.sl RenderMan shader source for the next two steps. The Evaluate function is where the guts of the shader will be written. RenderMan's Color variables should be declared as a single dimension array with 3 elements. Other variables need to be declared as doubles.

    These are the variables used to retrieve the values from the EnvData and EnvData3 parameters. They are usually equivalent to the function parameters found in the RenderMan shader.

    double tfactor;
    double Ctop[3], Cmtop[3], Cmlow[3], Clow[3];


    These variable declarations are taken almost directly from the declarations in the RenderMan shader file. I chose to deal with colors and transparency a little different than the RenderMan shader, so a few of the declarations in that shader are not used, or are renamed (e.g. surface_opac in the RenderMan shader has been renamed to txtr_opac below, just so you know I am going to do something a little different with that variable, rather than a direct translation from the RenderMan source).

    double txtr_opac;
    double width, cutoff, fade, f, turb, maxfreq = 16;
    double flame;
    double ss, tt;
    int i;


    These variables are used for temporary calculations and will be used as parameters for various function calls.

    double C[3], Cblack[3];
    double knot[24];
    double vec[2];


  15. (Evaluate_Fire2D function) The GetRGlobals function call at the beginning of the Evaluate function is used to store RenderMan global values into the RmanGlobal structure (rglobal). You can find out what RenderMan globals are currently supported by looking at the shd_txtr.h file. So for instance the the RenderMan global P (current position) is referred to as rglobal.P; Cs (surface color) is referred to as rglobal.Cs.

    There are many math functions the same or similar to RenderMan math functions, check out shd_math.h for a list of all the existing functions. Some functions (e.g., noise2, Cspline) use slightly different parameters than their RenderMan counterpart, but the functions have been made to look as much like the RenderMan functions as possible.

    If the texture opacity is 0 (or less), there is no need to do any calculations.

    if (rglobal.Ot > 0) {


    Because envelopes are being used, we need to evaluate all the Fire2D instance parameters before we are ready to do the shading calculations. I want the Time Factor parameter to be 1.0 when the flame speed looks good when running at 30 fps, so I do an adjustment at this point to bring it into line. (It could easily have been done in the ss and tt calculation below, but I wanted the shader code below to look like the RenderMan code for this tutorial).

    tfactor = eval_EnvData (&inst->timefactor, currentTime) * .005;


    For your shader to support the Texture Opacity and Texture Falloff parameter, you need to mix any colors used by your shader with the current surface color (rglobal.Cs), the amount of mixing being determined by the texture opacity (rglobal.Ot) value.

    eval_EnvData3 (&inst->topcolor, currentTime, C);
    mix (Ctop, rglobal.Cs, C, rglobal.Ot);
    eval_EnvData3 (&inst->midtopcolor, currentTime, C);
    mix (Cmtop, rglobal.Cs, C, rglobal.Ot);
    eval_EnvData3 (&inst->midlowcolor, currentTime, C);
    mix (Cmlow, rglobal.Cs, C, rglobal.Ot);
    eval_EnvData3 (&inst->lowcolor, currentTime, C);
    mix (Clow, rglobal.Cs, C, rglobal.Ot);


    Cblack is the color I am using anywhere the flame has no color. By mixing it with the surface color based on the surface transparency value, the flame can be overlayed on top of any existing texture already on the surface. This is one of the color and transparency differences I was refering to in the previous step, so .

    Vec3CopyC(Cblack, 0.0);
    mix(Cblack, rglobal.Cs, Cblack, sa->transparency);


    Now that we've got all the variables initialized correctly, we are ready to start the shader calculations. Use Shade's partialFrame global variable rather than the RenderMan fire function's parameter, frame.

    ss = rglobal.s * 5 + partialFrame * tfactor;
    tt = rglobal.t + partialFrame * (10 * tfactor);


    OK, I'll admit, here's one that needs to be explained to me. This is an approximation to the RenderMan shader's width calculations (filterwidth). I arrived at this calculation by observation, so use it if you'd like, or find the perfect one for your shader and use it. (If someone can explain the exact correlation between spotSize and filterwidth, we should create our own filterwidth functions.)

    width = 4.0 * sa->spotSize;


    Back to a fairly direct translation of the RenderMan source. Notice that because of a conflict with the C standard math library, the absoulte value function you should be using with doubles is Abs (instead of abs). The vec array is used to collect the ss and tt calculations, so they can be passed to the Shades' noise2 routine.

    turb = 0;
    cutoff = clamp(0.5 / width, 0, maxfreq);
    for (f = 1; f < 0.5 * cutoff; f *= 2) {
    vec[0] = ss * f; vec[1] = tt * f;
    turb += Abs(noise2(vec) - 0.5) / f;
    }
    fade = clamp(2.0 * (cutoff - f) / cutoff, 0, 1);
    vec[0] = ss * f; vec[1] = tt * f;
    turb += fade * Abs(noise2(vec)) / f;
    turb *= 0.5;

    flame = clamp(rglobal.t - turb, 0, 1);
    txtr_opac = flame * 1.5;


    Cspline is the Shades' routine that will choose a color from a spline curve based on the value in flame. The chosen color will be stored in the temporary variable C. The spline has 8 knots, and the knot variable needs to be initialized with the appropriate colors before calling Cspline.

    knot[0] = Ctop[0]; knot[1] = Ctop[1]; knot[2] = Ctop[2];
    knot[3] = Ctop[0]; knot[4] = Ctop[1]; knot[5] = Ctop[2];
    knot[6] = Ctop[0]; knot[7] = Ctop[1]; knot[8] = Ctop[2];
    knot[9] = Ctop[0]; knot[10] = Ctop[1]; knot[11] = Ctop[2];
    knot[12] = Cmtop[0]; knot[13] = Cmtop[1]; knot[14] = Cmtop[2];
    knot[15] = Cmlow[0]; knot[16] = Cmlow[1]; knot[17] = Cmlow[2];
    knot[18] = Clow[0]; knot[19] = Clow[1]; knot[20] = Clow[2];
    knot[21] = Clow[0]; knot[22] = Clow[1]; knot[23] = Clow[2];
    Cspline(C, flame, 8, knot);


    Finally we need to mix the flame color with the "non-flame" color, and assign that to the surface color that this shader returns for this point (sa->color). We also change the surface transparency at this point by assigning a new value to sa->transparency.

    mix(sa->color, Cblack, C, txtr_opac);
    sa->transparency = clamp(sa->transparency - txtr_opac, 0, 1);
    }


  16. (Interface_Fire2D function) The remainder of the steps all deal with the lwpanels interface for the shader. These declarations are used to assign envelope interface information into the appropriate EnvData structure. The first string is the envelope panel name, and the second string is the envelope channel name (only one for EnvData types). The two float values are the minimum and maximum values to be displayed by the envelope editor.

    ehInterfaceData tfIfaceData = {NULL,"Time Factor Envelope",{{"Time Factor",0.0f,1.0f},{NULL}}};


  17. (Interface_Fire2D function) Similar declarations for all EnvData3 variables. These structures require 3 envelope channels, but otherwise the format is the same as above.

    ehInterfaceData tpIfaceData = {NULL,"Top Color Envelope",{{"Red Channel",0.0f,256.0f},
    {"Green Channel",0.0f,256.0f},{"Blue Channel",0.0f,256.0f},{NULL}}};

    ehInterfaceData mtpIfaceData = {NULL,"Midtop Color Envelope",{{"Red Channel",0.0f,256.0f},
    {"Green Channel",0.0f,256.0f},{"Blue Channel",0.0f,256.0f},{NULL}}};

    ehInterfaceData mlwIfaceData = {NULL,"Midlow Color Envelope",{{"Red Channel",0.0f,256.0f},
    {"Green Channel",0.0f,256.0f},{"Blue Channel",0.0f,256.0f},{NULL}}};

    ehInterfaceData lwIfaceData = {NULL,"Low Color Envelope",{{"Red Channel",0.0f,256.0f},
    {"Green Channel",0.0f,256.0f},{"Blue Channel",0.0f,256.0f},{NULL}}}


  18. (Interface_Fire2D function) Here is where the declarations in the previous 2 steps are actually assigned to all the EnvData and EnvData3 variables. Notice the extra parameter in the init_EnvGui3 function, and refer to the comments in the source code for an explanation of its use.

    init_EnvGui (&inst->timefactor, &tfIfaceData, panels);
    init_EnvGui3 (&inst->topcolor, ENV3_COLOR, &tpIfaceData, panels);
    init_EnvGui3 (&inst->midtopcolor, ENV3_COLOR, &mtpIfaceData, panels);
    init_EnvGui3 (&inst->midlowcolor, ENV3_COLOR, &mlwIfaceData, panels);
    init_EnvGui3 (&inst->lowcolor, ENV3_COLOR, &lwIfaceData, panels);


  19. (Interface_Fire2D function) Just change the string to the name you want for your shader's main panel.

    if (panMID = PAN_CREATE (panels, "2D Fire Shader")) {


  20. (Interface_Fire2D function) This is where the name and positions of each of your shaders parameter requesters are defined. The Fire2D shader just needs a simple single column (single value) EnvData requester, followed by the color parameters. The Planks2D and Parquet2D plugins have good examples of the best way to handle 2-columns of parameters with the color parameters at the bottom. Use the pre-defined column constants (SPC_COL1, SPC_COL2, and SPC_COL3) and the vertical spacing constants (NGRP_VSPACE and GRP_VSPACE) from the shd_gui.h file to help position your requesters.

    if (!(draw_EnvGui(&inst->timefactor,panels,panMID,"Time Factor",SPC_COL1,curY))) goto err;
    curY += NGRP_VSPACE;
    if (!(draw_EnvGui3(&inst->topcolor,panels,panMID,"Top Color",SPC_COL3,curY))) goto err;
    curY += GRP_VSPACE;
    if (!(draw_EnvGui3(&inst->midtopcolor,panels,panMID,"Mid Top Color",SPC_COL3,curY))) goto err;
    curY += GRP_VSPACE;
    if (!(draw_EnvGui3(&inst->midlowcolor,panels,panMID,"Mid Low Color",SPC_COL3,curY))) goto err;
    curY += GRP_VSPACE;
    if (!(draw_EnvGui3(&inst->lowcolor,panels,panMID,"Low Color",SPC_COL3,curY))) goto err;


  21. (Interface_Fire2D function) Make the values in all your shader's variables appear in the appropriate requester.

    set_EnvGui (&inst->timefactor);
    set_EnvGui3 (&inst->topcolor);
    set_EnvGui3 (&inst->midtopcolor);
    set_EnvGui3 (&inst->midlowcolor);
    set_EnvGui3 (&inst->lowcolor);


  22. (Interface_Fire2D function) Update the values in all your shader's variables, based upon the changes made by the user through the lwpanels interface.

    get_EnvGui (&inst->timefactor);
    get_EnvGui3 (&inst->topcolor);
    get_EnvGui3 (&inst->midtopcolor);
    get_EnvGui3 (&inst->midlowcolor);
    get_EnvGui3 (&inst->lowcolor)


    That's the end of the modifications necessary to create your own Shades plugin, so save the modified file. Now, just a couple of more changes necessary to add the shader routines you just wrote, to the rest of the Shades package.


Edit the shades.c file.
 
  1. Include your shader's .h file at the top of this list.

    #include "fire2d.h"


  2. Add your two new servers to the top of this list.

    {"ShaderHandler", "ML_Fire2D", Activate_Fire2D},
    {"ShaderInterface", "ML_Fire2D", Interface_Fire2D},


    Save these changes, and your two new servers (Activate_Fire2D and Interface_Fire2D) will be built into the Shades plugin at compile time. That completes all the necessary changes to the source code, all that remains is to make sure these new files are included when we build the project.


Add the new files to the project, and build it.
 
Choose the appropriate step below for your platform and/or compiler.

  1. Microsoft Visual C++ 5.0 programmers:

    Add the 2 new files, fire2d.c and fire2d.h to the project, by choosing the "Add to Project... Files" option under the Projects menu.

    Now just choose the "Build shades.p" option from the Build menu.

    The Visual C++ project that is distributed with Shades is setup to build the shades.p file in the Release subdirectory. If you want to make this a Debug project instead, make the appropriate changes in the "Project... Settings" panels.


  2. SGI Irix programmers:

    Edit the Makefile and add your .h and .o filenames to the beginning of the TXTINCS and TXTOBJS lines.

    TXTINCS = fire2d.h parquet2d.h planks2d.h
    TXTOBJS = fire2d.o parquet2d.o planks2d.o


    Save the Makefile and type "make" in the Unix shell. The .p file should be created in the current directory.


 

(RenderMan is a registered trademark of Pixar)

(Lightwave3D is a registered trademark of Newtek, Inc.)

 

s i t e d e s i g n - v i s u a l e y e s