|
Features

Implementing Modular HLSL with RenderMonkey
HLSL
with RenderMonkey
When
you first open RenderMonkey, you'll be greeted with a blank workspace.
The first thing to do is create an Effect Group. To do this, right-click
on the Effect Workspace item in the RenderMonkey Workspace
view and select Add Effect Group. This will add a basic Effect
Group that will contain editable effects elements. If you have the
same capabilities as the default group (currently a RADEON 8500,
GeForceFX or better) then you'll see a red teapot. If you're running
on older hardware (like a GeForce3) then you'll have to edit the
pixel shader version in the default effect from ps 1.4 to ps 1.1.
RenderMonkey
automatically creates a vertex stream mapping for the positional
data of the model, places the view/projection matrix in a shader
constant for you, and creates the high level vertex and pixel shaders
for you. The default vertex shader is shown in below:
Both
the high-level vertex and pixel shader editor windows have three
areas. The top area lets you manage the interface between "external"
parameters (either RenderMonkey supplied or user-created variables)
and the shader and lets you pick the target shader version. The
middle area is a read-only area that shows the parameter declaration
block used by the HLSL. When you add a parameter to an effect, it
will become available as an external parameter, and the parameter
declaration block lets you see the association between these parameters
and the shader registers. The bottom area contains the actual shader
code that you edit directly. In Figure 1, you can see that the RenderMonkey
supplied view/projection matrix is mapped to shader constant c0
(c0 though c3 is implied by the float4x4
mapping), and this name is used in the actual vertex shader. These
variables can be considered global declarations. The input variables
from the vertex stream show up as the parameters to the entry point
function, typically called main.
As
you can see in the Figure 1, RenderMonkey has provided the minimal
shader as the default. The default vertex shader transforms the
incoming vertex position by the view/projection matrix while the
default pixel shader (not shown) sets the outgoing pixel color to
red. You can edit the shader code in the lower window till you get
the shader you want. To see what the shader looks like, click on
the Commit Changes button on the main toolbar (or press F7)
to internally save and compile the shader. If the shader has any
errors, there will be an informative message displayed in the output
pane at the bottom of the RenderMonkey window. If the shader compiled
successfully, then you'll immediately see the shader results in
the preview window.
And
that's about all you need to know to edit shaders in RenderMonkey!
The interface is very intuitive - just about everything can be activated
or edited by double-clicking. You can insert nodes to add textures,
set render state, or add additional passes with just a few clicks.
The documentation for RenderMonkey comes with the RenderMonkey download
and is also available on
this page, along with a number of documents on using RenderMonkey.
Finally,
you'll need to know some internal variables that are available to
RenderMonkey, shown in Figure 2. If you add the RenderMonkey names
(case sensitive) as variables they'll be connected to the internal
RenderMonkey variables. The time-based values are vectors, but all
elements are the same value. You can use these to vary values programmatically
instead of connecting a variable to a slider.

Writing Modular Code in HLSL
If
you've been writing low-level shader code, you probably haven't
been thinking about writing modular code. It's tough to think modularly
when you don't have any support in the language for any type of
control statements. And surprisingly, there's still no actual support
for modular code. A shader written in HLSL still compiles to a monolithic
assembly shader. However, the HLSL compiler does hide a lot of the
details and does let you write like we can write a modular
shader. I mention this because it's easy to get lulled into thinking
that you're working with a mature language, not one that's less
than a year old. You should be aware of these limitations. There's
no support (yet) for recursion. All functions are inlined. Function
parameters are passed by value. Statements are always evaluated
entirely - there's no short-circuited evaluation as in a C program.
Even
with those limitations, it's surprisingly easy to write modular
code. In Wolfgang Engel's article, he discussed the lighting equation
for computing the intensity of the light at a surface as the contribution
of the ambient light, the diffuse light and the specular light.

I've
made a slight change by adding in a term for the light color and
intensity, which multiplies the contributions from the diffuse and
specular terms and by using I for intensity and C for color. Note
that the color values are RGBA vectors, so there are actually four
color elements that will get computed by this equation. HLSL will
automatically do the vector multiplication for us. Wolfgang also
created a HLSL shader for this basic lighting equation, so if you're
new to HLSL you might want to review what he wrote, since I'm going
to build on his example.
Let's
rewrite the basic shader, setting things up so that we can modularize
our lighting functions. If I add a color element to the output structure
(calling it Color1),
we can edit the main function to add in the vertex normal as a parameter
from the input stream and write the output color. Insert two scalar
variables, Iamb for
ambient intensity and Camb
for ambient color (correspond the above equation) in the RenderMonkey
workspace. This will allow us to manipulate these variables from
RenderMonkey's variable interface. RenderMonkey has a very nice
interface that supports vectors, scalars, and colors quite intuitively.
To implement the lighting equation we'll need to compute the lighting
vector and the view vector, so I added these calculations for later
use. The ambient lighting values and light properties (position
and color) need to be provided to RenderMonkey by assigning them
to variables. The basic vertex shader computing the output color
from the product of the ambient intensity and the ambient color
looks like this.

Note
that vector is a
HLSL native type for an array of four floats, it's the same as writing
float4. Also note the use of swizzles when calculating the
normalized vectors - this leaves the vector's w parameter out of
the calculation. I also modified the default pixel shader to simply
pass along the color created in the vertex shader as shown below.
This simple pixel shader simply returns the (interpolated) color
provided by the vertex shader.

Functions in HLSL
So
let's start off by making the ambient calculation a function just
to see how it's done in HLSL. Making the ambient calculation a function
is pretty simple.

The
static inline attributes
are optional at this point, but I've placed them there to emphasize
that currently all functions are inlined, so creating and using
a function like this adds no overhead to the shader. This Ambient()
function just computes the ambient color and returns it.
Creating
the Diffuse function requires that we pass in the lighting vector
and the normal vector. In addition to the argument type description
you'd expect to see in a C program, HLSL allows you to specify if
a value is strictly input, output or both through the in,
out and inout
attributes. A parameter that is specified as out
or inout will be
copied back to the calling code, allowing functions another way
to return values. If not specified, in
is assumed. Since this diffuse equation is an implementation of
what's called a Lambertian diffuse, I've named it as such. The LambertianDiffuse()
function looks like this.

Note
the use of the HLSL intrinsic dot product function. The specular
equation is taken from Phong's lighting equation and requires calculation
of the reflection vector. The reflection vector is calculated from
the normalized normal and light vectors.

The
dot product of the reflection vector and the view vector is raised
to a power that is inversely proportional to the roughness of the
surface. This is a more intuitive value than letting a user specify
a specular power value. To limit the specular contribution to only
the times when the angle between these vectors is less than 90 degrees,
we limit the dot product to only positive values. The specular color
contribution becomes;

Implementing
this in HLSL looks like the following:

Note
the use of the intrinsic saturate function to limit the range from
the dot product to [0,1]. Roughness is added to the RenderMonkey
Effect Workspace and added in the shader editor as a parameter.
Using
these functions we can now implement our main shader function as
follows:

The
three functions that we added are either placed above the main function
or below, in which case you'd need to add a function prototype.
As you can see, it's fairly easy to write functional modules in
HLSL code.
Finally, Modular Code
The
real utility of this comes when we create modules that can replace
other modules. For example, suppose that you wanted to duplicate
the original functionality of the fixed-function-pipeline, which
implemented a particular type of specular called Blinn-Phong. This
particular specular lighting equation is similar to Phong's but
uses something called the half-angle vector instead of the reflection
vector. An implementation of it looks like this:

To
change our shader to use Blinn-Phong, all we need to do is change
the function we call in main.
The color computation would look like this;

Since
all of these functions are inlined, any unused code is optimized
out from the shader. As long as there's no reference to a function
from main or any
of the functions that are called from main,
then we can pick which implementation we want in our shader code
simply by selecting the functions we want, and we don't have to
worry about unused code since it's not included in the compiled
shader.
As
we get more real-time programmability it becomes easier to implement
features that have been in the artist's domain for years. Suppose
your art lead creates some really cool scenes that look great in
Maya or 3DS Max, but don't look right because the Lambertian diffuse
in your engine makes everything look like plastic? Why can't you
just render with the same shading options that Maya has? Well, now
you can! If your artist really has to have gentler diffuse tones
provided by Oren-Nayar diffuse shading, then you can now implement
it.
______________________________________________________
|