|
[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, Brenden's web pages seem to be 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
|