| |
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.
-
-
-
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.
-
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).
-
-
-
The first MODIFY comment describes the same global string
replace as in the .h file.
-
Change the name of the include file to your shader's .h file.
#include "fire2d.h"
-
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.
-
(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;
-
(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);
-
(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);
-
(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];
-
(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),
...);
-
(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);
-
(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);
-
(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.
-
(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.
-
(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);
-
(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];
-
(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);
}
-
(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}}};
-
(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}}}
-
(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);
-
(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")) {
-
(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;
-
(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);
-
(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.
-
-
-
Include your shader's .h file at the top of this list.
#include "fire2d.h"
-
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.
-
-
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.
-
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
|
|