TUT: Shader Programming 101


This page is an update of the Nanomation Shader Programming 101 tutorial (http://www.nanomation.co.uk/Programming-101.html).

Table of contents

How material shaders work

Mental ray works by shooting a series of eye rays from the camera into your scene and testing to see if a ray hits any of the scene objects. If so, the program works out where the ray touches the object, finds the objects material shader, and calls it passing a bunch of information about the state of the scene. The material shader then decides what the surface colour should be at that point, and passes the information back to mental ray.

A material shader typically does complex calculations involving the objects surface characteristics and the orientation of the point hit by the ray with respect to the scene lights. It may also fire new rays itself to simulate reflections and transparency. In this first tutorial the shader will return the same colour no matter where the ray hits the object. It will produce the same effect as the built-in constant shader. Then you will modify the code to produce some more interesting effects based on the scene state information passed by mental ray.

Creating the Shader

Define the Shader Type

  1. Click Views > Layouts > Tools Development Environment.

  2. Click the Plug-ins tab and do one of the following:

    • To create the shader in the User location, click File > New > Shader.
    • To create the shader in a different location (such as a workgroup or an Add-on directory), expand the location in the Plug-in Tree, right-click the SPDL folder and click New > Shader.
  3. Let's call the shader "basic_material". Type "basic_material" in the Short Name and Long Name boxes.
  4. In the Main Group list, click Material.
  5. Under Material Type, clear the Shadow and Photon Material check boxes.

Add Parameters

The Add Parameters tab defines the shader parameters that the user can pass to the shader from the shader property page. Our simple shader is going to have one parameter only: the color of the object.


  1. Click the Add Parameter tab.
  2. In the Type list, click Color (alpha).
  3. In the Name box, type col, and in the Label box, type Object Color.
  4. In the Default Color control, set Red and Alpha to 1.0, and leave Green and Blue set to 0. These are the initial values that will be displayed when a user first opens the property page.
  5. Now, click Add Parameter to add the color parameter to the list of parameters. You can always edit the parameter definition later, and click Update Parameter to update the definition in the parameter list.

If our shader had more parameters, we could use the Define Layout tab to customize the layout of the UI controls on the property page. But since our basic_material shader has just one parameter, we don't need to do anything special with the layout.

Generate the Project

  1. On the Shader Information tab, click Generate Project to generate the SPDL, C++, and .VCPROJ files for the shader.

    All the generated files are put in the folder specified in the Output Directory box of the shader wizard. By default, the output directory is the Application\spdl folder.

  2. In the Plug-in Manager, click the SPDLs tab and then click Update All. Now go back to the Plug-in Tree, where you should see the basic_material.spdl file.

    Shader SPDL file in the Plug-in Tree

  3. Open the Visual C++ project:

    1. Right-click the SPDL node and then click Command Prompt. This opens an XSI command prompt.

      If the location of a workgroup is a UNC path (such as \\server\My Workgroup) then you'll have to open an XSI command prompt from the Windows Start menu.

    2. In the command prompt, run the following command:

      devenv basic_material.vcproj
      
      Important The generated Visual C++ project uses XSI environment variables such as XSISDK_ROOT. So you should always start Visual C++ from an XSI command prompt, because then the XSI environment variables are already set.

  4. In the Source Control dialog box, click OK.
  5. In the Change Source Control dialog box, click Work Disconnected.
  6. In the Solution Explorer, double-click basic_material.cpp to load the file into the source code editor.

    Description

Edit the Shader Code

The file defines four functions - basic_material_version; basic_material_init; basic_material; and basic_material_exit. We are going to modify basic_material. This is the function called by mental ray every time a camera ray hits our object. It is worth having a look at this function in detail now.

extern "C" DLLEXPORT miBoolean
basic_material
(
	miColor				*result,
	miState				*state,
	basic_material_t	*params
	)
{
	// TODO: Shader main code goes here
	
	return( miTRUE );
}


Shaders compile into DLLs, so the function type has to be DLLEXPORT, The return value is type miBoolean - generally you will return miTrue to mental ray if the shader completes without an error. The ray tracer passes three parameters to your shader - result is where you will store the surface colour you're going to compute. state is a huge structure containing all the information mental ray has about the state of the scene, the ray that hit the object, and the object itself. The final parameter is params - a structure containing the parameters passed from the shader dialog box. This structure is defined in basic_material.h - have a quick look at that file now. The relevant part is:

typedef struct
{
	miColor				col;				// Object Color
} basic_material_t;

As you can see, the structure contains one variable called col of type miColor.

Change the basic_material function as follows:

DLLEXPORT miBoolean basic_material
(
    miColor * result,
    miState * state,
    basic_material_params * params
)
{

    // TODO: Shader main code goes here
    *result = *mi_eval_color(&params->col);

    return miTRUE;
}


The line that does the work is:

 *result = *mi_eval_color(&params->col);


The shader calls the mental ray function mi_eval_colour() to read in the values from the dialog box. All input values are read in this way - the function decides if the dialog box slider values should be read directly, or if another shader is plugged into the parameter in the render tree, in which case that shader is retrieved and called. All this happens transparently without any further action by the shader programmer, and makes life very simple. The function takes as its parameter the address of our col variable and returns the address of the result of its calculation.

Note that any variables or temporary values created in the shader will be created on the stack, so the return values must be explicitly copied to result. It is not enough to simply copy the address of the mi_eval_color() return value, as the contents will be destroyed when the shader returns, leading to garbage in the shader output.


Compile the Shader

  1. Change the active build configuration to Release.

    Description

  2. Change the project so that the compiled DLL is automatically put in the correct location. Click Project > basic_material Properties, then on the Linker > General configuration page, change the Output File to ..\bin\$(XSI_CPU)\basic_material.dll.
  3. Click Build > Build Solution to compile the shader.
  4. The first time you compile the shader, you are prompted to save a solution (.sln) file. Just click Save to save the .sln file.

Generate a Preset

  • In the Plug-in Tree, right-click the basic_material SPDL file and then click Regenerate Preset.

    Description

If you want to know where the preset file is, right-click the SPDL file and then click Browse Preset Directory.


Apply the Shader

  1. In the Plug-in Tree, right-click the basic_material SPDL file and then click Browse Preset Directory.
  2. Click the XSI tab to display the XSI viewports.
  3. Create a primitive polygon mesh sphere.
  4. Drag basic_material.preset to the sphere.
  5. Drag a render region around the sphere and you should see something like the following:
Description
Try moving the sliders to check that the sphere changes colour. Have you ever had so much fun?

In general it is a pain navigating through the user hierarchies trying to find shader presets. There are two ways round this - either drag the preset onto a custom toolbar - a large black square appears on the toolbar, and clicking it will apply the shader to whatever is selected at the time. The black square isn't particularly intuitive though, especially if you have several presets on the same toolbar. An alternative (and better) method is to create a script button running the command

ApplyShader "basic_material.Preset"

More on all that later.

The State Structure

Earlier I touched on the state structure, and said it was how mental ray passes information about the state of the scene to your shader. The structure is defined in shader.h, which is #include-d at the beginning of your code. Highlight the text shader.h in the include statement, right click it, and select Open Document shader.h.. The structure miState is defined about half way down the file. There's no room here to describe the structure in detail - see Programming Mental Ray (Th.Drietmayer and R Herken) for more information.

You will need this book if you are serious about writing shaders. There is an html version in XSI_5.11>doc>mental_ray>manual but a printed version will save you a lot of time. I think there used to be a pdf version on the install disks - I'm not sure if it's still there.

We are going to modify the basic_material shader to read some information from the state structure and make pretty pictures with it.

Close down XSI, open up Visual C++ again, and re-load the project file basic_material.dsw. Modify the basic_material function as follows:


extern "C" DLLEXPORT miBoolean
basic_material
(
	miColor				*result,
	miState				*state,
	basic_material_t	*params
	)
{
	// TODO: Shader main code goes here

        result->r = state->normal.x;
        result->g = state->normal.y;
        result->b = state->normal.z;
        result->a = 1.0f;

	return( miTRUE );
}

The new code takes the surface normal direction of the object at the point hit by the ray, and copies it into the output colour returned by the shader. The normal vector is made up of x, y, and z components, each in the range -1.0 to +1.0. Mental ray treats any negative colour values as if they were zero, so I just copy the values straight into the r, g, and b components of the output colour.

The alpha is set to 1.0 to make the object completely opaque. The ray tracer always expects material shaders to return an alpha value, and may make decisions based on it. Make sure you always return something sensible.

Compiling and installing the new shader code

Compilation should now be very straightforward - simply hit F7 or pick Rebuild All from the Build menu. Before installing the shader you need to un-install the previous version. In an XSI command prompt window, change directory to the shader code folder, and type:

xsi -u basic_material.spdl xsi -i basic material.spdl to uninstall the old version and re-install the new one.

Make sure you have shut down XSI before doing the install / uninstall process. If you get errors saying that XSI is unable to delete or install libraries it probably means that you have the program still running.

Shader programming involves a lot of closing down and re-starting XSI, because it doesn't un-link the shader DLLs when it has finished rendering - perhaps for performance reasons. I have asked Softimage to consider adding an UnlinkShaderDLLs button somewhere, but it's a kind of obscure request, so I'm not holding my breath.


Using the new shader

Start up XSI again and create a default polygon sphere. Open a Script Editor (click the scroll button Description next to Playback on the bottom bar), type the following ApplyShader command, and click Run.

' VBScript
ApplyShader "basic_material.Preset"

If you prefer JScript (which if you're a programmer you really should do) type:

//JScript
ApplyShader ("basic_material.Preset");

Drag a render region around the sphere and have a look from various angles.

Description

As you can see, the sphere is red in the direction of positive x, green in the direction of positive y, and blue in the direction of positive z - check the XYZ arrows at the bottom left of the picture. Have a look on the back of the sphere as well.

While you have the script window open, select the command you have just typed in and drag it onto a custom toolbar - either the one below the palettes on the command bar, or create a new one using View>Custom_Toolbars>New_Toolbar. Name the button Basic Material and name the command basicMaterialCommand. Change the name of the script file as well, to basicmaterialcommand.vbs or basicmaterialcommand.js. This is my preferred method of using custom shaders, and if you package shaders as addons you can include the toolbar and buttons as well.

The normal vectors displayed by the shader above are actually the interpolated normals. Mental ray has assumed that the sphere is to be smooth shaded, and has 'guessed' the values after taking into account the normals of the actual polygons that make up the geometry, the discontinuity setting of the Geometry Approximation property, etc.

If you want to see the actual normals of the polygons:

  1. Exit XSI.
  2. In the basic_material.cpp, change the basic_material function to:

    extern "C" DLLEXPORT miBoolean
    basic_material
    (
    	miColor		*result,
    	miState		*state,
    	basic_material_t	*params
    	)
    {
    	// TODO: Shader main code goes here
    	result->r = state->normal_geom.x;
    	result->g = state->normal_geom.y;
    	result->b = state->normal_geom.z;
    	result->a = 1.0f;
    
    	return( miTRUE );
    }
    
  3. Rebuild the DLL.
  4. Now start up XSI again, create a default sphere and click the Basic_Material button on your custom toolbar.


Now you can see each individual facet. Try it on NURBS objects too.

Description


Notice how some of the polygons seem to be darker at the top and lighter at the bottom? In fact they're not - cover up the polygons immediately above and below a suspect one and you can see it's actually an even colour all over. It is an optical illusion called (incredibly) the Craik-Obrien-Cornsweet Effect. There's an explanation of it here (http://persci.mit.edu/people/adelson/publications/gazzan.dir/gazzan.htm).

A Third Variation

Another easy change you can make involves barycentric coordinates. They are difficult to explain concisely, but are a by-product of the normal interpolation process. Mental ray converts all geometry into triangles before rendering actually begins, because triangles are much more efficient to process than other polygons or NURBS surfaces.You can define a point inside a triangle in terms of its distance from each of the three vertices - these distances are the barycentric coordinates. When a material shader is called the ray intersection points' barycentric coordinates inside its particular triangle are stored in the state structure in the array bary[0..2]. These coordinates are in the range 0.0 - 1.0, making them prime candidates for copying into a colour structure...

Again, open up basic_material.cpp, and find the basic_material function. Modify it as follows:

extern "C" DLLEXPORT miBoolean
basic_material
(
	miColor		*result,
	miState		*state,
	basic_material_t	*params
	)
{
	// TODO: Shader main code goes here
	result->r = state->bary[0];
	result->g = state->bary[1];
	result->b = state->bary[2];
	result->a = 1.0f;

	return( miTRUE );
}

Exit XSI and rebuild the shader. Fire up XSI again and apply the shader to a default sphere:

Description

As you might be able to see, the barycentric coordinate of a point for each triangles vertex is 1.0 at the vertex itself, and 0.0 at anywhere along the triangle edge opposite the vertex. This property is quite useful, because it provides an easy way of displaying the results of mental rays triangulation process and displaying a wireframe view of the resulting triangles.

This page was last modified 20:40, 14 Dec 2010.
This page has been accessed 38287 times.

© Copyright 2009 Autodesk Inc. All Rights Reserved. Privacy Policy | Legal Notices and Trademarks | Report Piracy