One of the primary purposes of graphics cards is to render 3D objects to a 2D frame buffer to display. How the object is rendered is determined by what is called the "shading model", which is typically fed information such as the object's color and position, lighting color and positions, texture coordinates, and material characteristics like how shiny or matte the object should appear. The program that executes in hardware or software to perform this calculation is called the shader. Traditionally graphics cards have used a fixed shading pipeline for applying a shading model to an object, but in recent years, graphics cards have acquired programmable pipelines so that custom shaders can be executed in place of the fixed pipeline. For a summary off many of the ways to control the fixed OpenGL pipeline, see Tutorial 35: Lighting and Fog.
Hardware Requirement: To fully experience this Tutorial, you will need a graphics card that supports programmable shaders, e.g. ATI Radeon 9200, NVIDIA GeForce 5000 series or later graphics cards. It is also recommended that you update your OpenGL driver with the latest available for your graphics card. On Macintosh, this is provided with the latest OS update. On PC, you can acquire the latest driver from either your graphics card manufacturer or your computer manufacturer.
One of the simplest shading models which takes lighting into account is called
Flat Shading or
Facet Shading, where each polygon has a single color across the entire polygon based on surface normal and lighting properties. As we saw demonstrated in
Tutorial 35, this can be accomplished in Jitter by setting the
lighting_enable attribute of a Jitter OpenGL object (e.g.
jit.gl.gridshape) to
1.
• Open the Tutorial patch
41jShaders in the Jitter Tutorial folder. Click on the
toggle box labeled
Start Rendering.
• Click the
toggle box object above the
message box object reading
lighting_enable $1 to turn on lighting for the
jit.gl.gridshape object drawing the torus. The
smooth_shading attribute is
0 by default.
• Now you should see a change in the lighting of the torus. Instead of the dull gray appearance it started with, you will see a shiny gray appearance like this:
A rendered torus showing Flat shading.
While this might be a desirable look, the color of most objects in the real world changes smoothly across the surface. "Gouraud Shading" (1971) was one of the first shading models to efficiently address this problem by calculating a per vertex color based on surface normal and lighting properties. It then linearly interpolates color across the polygon for a smooth shading look. While the artifacts of this simple approach might look odd when using a small number of polygons to represent a surface, when using a large number of polygons, the visible artifacts of this approach are minimal. Due to the computational efficiency of this shading model, Gouraud Shading has been quite popular and remains the primary shading model used today by computer graphics cards in their standard fixed shading pipeline. In Jitter, this can be accomplished by setting the
smooth_shading attribute to
1 (on). Let's do this in our patch, increasing and decreasing the polygon count by changing the
dim attribute of the
jit.gl.gridshape object.
• Click the
toggle box attached to the
message box reading
smooth_shading $1. Try increasing and decreasing the polygon count by changing the
number box attached to the message box reading
dim $1 $1 in the lower right of the patch.
A torus rendered with smooth shading.
As mentioned, smooth shading with a low number of polygons has visible artifacts that result from just performing the lighting calculation on a per vertex basis, interpolating the color across the polygon. Phong Shading (1975) smooths not only vertex color across a polygon, but also smooths the surface normals for each vertex across the polygon, calculating a per fragment (or pixel) color based on the smoothed normal and lighting parameters. The calculation of lighing values on a per pixel basis is called "Per Pixel lighting". This is computationally more expensive than Gouraud shading but yields better results with fewer polygons. Per pixel shading isn't in the OpenGL fixed pipeline; however we can load a custom shader into the programmable pipeline to apply this shading model to our object.
• Load the per-pixel lighting shader by clicking the
message box that says
read mat.dirperpixel.jxs connected to the
jit.gl.shader object.
• Apply the shader to our object by clicking the
message box
shader shademe connected to the
jit.gl.gridshape object. This sets object’s
shader attribute to reference the
jit.gl.shader object by its
name (
shademe).
Per-pixel shading applied to the torus.
In 1984, Robert Cook proposed the concept of a shade tree, where one could build arbitrary shading models out of some fundamental primitives using a "shading language". This shading language made it so that a rendering pipeline could be extended to support an infinite number of shaders rather than a handful of predefined ones. Cook's shading language was extended by Ken Perlin to contain control structures and became the model used by Pixar's popular RenderMan shading language. More recently the GPU-focused Cg and GLSL shading languages were established based on similar principles. The custom shaders used in this tutorial, including the per-pixel lighting calculation, were written in GLSL.
A brief introduction to how to write your own shaders will be covered in a subsequent Tutorial. For now, lets continue to use pre-existing shaders and show how we can dynamically change shader parameters. Gooch shading is a non-photorealistic shading model developed primarily for technical illustration. It outlines the object and uses warm and cool colors to give the sense of depth desired for technical illustrations. We have a custom shader that implements a simplified version of Gooch Shading which ignores the application of outlines.
• Load the simplified Gooch shader by clicking the
message box
read mat.gooch.jxs connected to the
jit.gl.shader object.
The simplified Gooch shading model applied to our torus.
You should notice that the warm tone is a yellow color and the cool tone is a blue color. These values have been defined as defaults in our shader file, but are exposed as parameters that can be overridden by messages to the
jit.gl.shader object.
• Using the
number boxes in the patch, send the messages
param warmcolor <red> <blue> <green> <alpha> and
param coolcolor <red> <green> <blue> <alpha> to the
jit.gl.shader object to change the tones used for the warm and cool colors.
We can determine parameters available to the shader by sending the
jit.gl.shader object the message
dump params to print the parameters in the Max window, or by sending the message
getparamlist, which will output a parameter list out the object’s rightmost (dump) outlet. An individual parameter's current value can be queried with the message
getparamval <parameter-name>. A parameter’s default value can be queried with the message
getparamdefault <parameter-name>, and the parameter’s type can be queried with
getparamtype <parameter-name>.
Shaders can be used not only to determine the surface color of objects, but also the position and attributes of our object's vertices, and as we'll see in our next Tutorials. For now, let's look at vertex processing. In the previous shaders we just discussed how the different shaders would render to pixels. In fact, for each of these examples we were running two programs: one to process the vertices (the vertex program) and one to render the pixels (the fragment program). The vertex program is necessary to transform the object in 3D space (rotation, translation, scaling), as well as calculate per-vertex lighting, color, and other attributes. Since we see the object move and rotate as we make use of the
jit.gl.handle object in our patch, obviously some vertex program must be running. Logically, the vertex program runs on the programmable vertex processor and the fragment program runs on the programmable fragment processor. This model fits with the fixed function pipeline that also separates vertex and fragment processing tasks.
The custom vertex program in the previous examples, however, didn’t visibly perform any operation that is noticeably different than the fixed pipeline vertex program. So let’s load a vertex shader that has a more dramatic effect. The vd.gravity.jxs shader can push and pull geometry based on the vertex distance from a point in 3D space.
• Load the simplified gravity vertex displacement shader by clicking the
message box
read vd.gravity.jxs connected to the
jit.gl.shader object.
• Control the position and amount of the gravity vertex displacement shader by changing the
number boxes connected to the
pak object and
message box outputting the messages
param gravpos <x> <y> <z> and
param amount <n>, respectively.
In this tutorial we discussed the fixed and programmable pipelines available on the graphics card and demonstrated how we can use the programmable pipeline by loading custom shaders with the
jit.gl.shader object. Shaders can then be applied them 3D objects to obtain different effects. We can also set and query shader parameters through messages to the
jit.gl.shader object.