Yo Frankie! | Crystal Space Tutorials

Introduction to Crystal Space Shaders

Overview

This document describes the basic structure of a Crystal Space shader by successively adding more features that require different settings in the Crystal Space shader definition.

Cg will be used in the shader created. However, Cg is not the focus of this tutorial and is only explained tangentially.

Nomenclature

In different contexts and programs the word "shader" has different meaning. For example, in offline rendering it is used in a lot of places ("surface shader", "light shader") and is, generally, a description of appearance of something. In DirectX the programs running on the vertex resp. fragment units of graphics hardware are called "shaders" ("vertex shaders" and "pixel shaders"). OpenGL used to call the same "vertex programs" and "fragment programs", but recently also adopted the name "shader".

In Crystal Space, a shader somewhat follows the offline rendering nomenclature by basically being a flexible description of the appearance of a surface. "Flexible" means that a shader can take parameters which influence it's output. A simple example for that is a surface texture. The "description" of the appearance is (usually) given as a vertex and shader program.

Structure

A Crystal Space shader consists of multiple components, nested in each other.

Techniques

At the top level, a shader consists of one or more techniques. Each technique has an associated priority. At runtime, of all available techniques, the highest one supported by the used graphics hardware is used. The idea behind techniques is to provide the same, or similar, surface appearance described in multiple ways, fitting hardware with different capabilities. A common scenario is that a certain shader can be drawn in a single rendering pass on some hardware, but requires multiple passes on other.

Passes

Rendering passes are the next level below techniques. Each technique consists of one or more passes. Each pass can have different, individual rendering settings (such as the shader programs used). Passes have a specific order; when a mesh is rendered with a shader technique consisting of multiple passes, it is effectively rendered once for each pass, with the respective rendering settings applied for each time it is rendered. The order the passes are applied is the order they were defined.

Pass contents: programs and bindings

Inside passes most of the actually interesting settings can be found. Foremost, passes contain programs for software vertex processing (called "vertex processors"), hardware vertex processing (vertex programs) and hardware fragment processing (fragment programs). These are implemented through Crystal Space plugins and thus different implementations and languages are possible. Vertex and fragment programs can be provided as assembly (ARB vertex/fragment programs) or high-level shading language programs (Cg). However, also setups for the fixed-function pipeline are subsumed under "programs" - while technically inaccurate the abstraction is practical.

Passes also contain information on how data provided by Crystal Space is connected to the shader programs. These contain buffer bindings (linking buffers of standard and arbitrary per-vertex data to vertex program inputs) and texture bindings (linking textures to fragment program inputs). In addition, most program plugins also have a facility to map shader variable values to program inputs constant for one rendered mesh.

Furthermore, some rendering options can also be set, like Z buffer mode or blending mode.

Shader step by step

The following sections will cover the creation of a shader, in multiple steps. Basic elements will be added until the first "milestone" - a simple, working shader. Based on that more advanced features will be added.

To test the various steps of the development a simple CS program "shadertest" is provided along with this document. It is, in essence, a slightly modified "simple2" tutorial from Crystal Space, but with various code snippets to play around with the shader(s) created.

Basic Structure

Crystal Space shaders are XML-based. The root node for a shader is a shader node:

<shader compiler="xmlshader" name="simpleshader">
  ...
</shader>

Each shader needs to have a name and a compiler attribute. The compiler specifies what plugin is used to interpret the shader XML. The currently available compilers are xmlshader and shaderweaver. xmlshader provides the explicit technique/pass/program-based model described in this document. shaderweaver takes a shader described as a graph from which a "classic" Crystal Space shader is generated. It is not covered in this document.

A shader must have at least one technique:

<technique priority="100">
  ...
</technique>

The priority is an integer value which sets an order of techniques: the technique with the highest priority is attempted to be loaded; if loading it is used whenever the shader is used for rendering. If loading does not succeed, the technique with the next lowest priority is used. And so on, until a priority is found or no further priorities are available (in which case searching is continued in the optional fallback shader). Note that the order techniques are defined does not matter, they are ordered by their priorities when a shader is loaded.

Also, a shader needs at least one pass:

<pass>
  ...
</pass>

A pass node doesn't have any attributes. Keep in mind that, unlike technique nodes, their order matters.

Simple Programs

The parts primarily responsible for the effect of a shader are the shader programs. Here, we will add programs for the simplest shader: rendering an object in a solid color.

The programs here written will be in Cg. If you are familiar with C or any language with some relation to it (C++, Java, ...) the syntax should be easy to understand.

To add a program, you first need to add a node for it's type. We start with the vertex program, for that we add:

<vp plugin="glcg">
  ...
</vp>

The plugin attribute specifies the shader program plugin to use. For Cg programs this must be glcg. Other plugins you can encounter in the shaders shipped with Crystal Space are glfixed (for fixed function setups) and glarb (for ARB assembly shaders), however, neither will be further explained in this document.

The contents under the vp node are entirely plugin-dependent. Cg vertex programs require another nested node:

<cgvp>
  ...
</cgvp>

The program itself is specified in a program node:

<program>
<![CDATA[
float4 main (float4 positionObj : POSITION,
        uniform float4x4 ModelViewProj : state.matrix.mvp) : POSITION
{
        return mul (ModelViewProj, positionObj);
}
]]>
</program>

This little program merely transforms the position of a vertex (in object space) into camera space (where the point if the viewer is at the origin, looking down the Z axis). The POSITION keyword (a Cg "semantic" on the positionObj parameter signals that the values for it should come from the vertex position per-vertex data. The way ModelViewProj is defined binds it to the combined object-to-camera and projection matrices. Finally, the POSITION on the return value indicates the output is the transformed vertex position. [1]

[1]For more details refer to the Cg user manual or one of the Cg tutorials on the web.

The fragment program follows the same pattern:

<fp plugin="glcg">
  <cgfp>
    <program>
    <![CDATA[
    float4 main () : COLOR
    {
            return float4 (1, 0.8, 0, 1);
    }
    ]]>
    </program>
  </cgfp>
</fp>

Here the COLOR indicates that the return value is the output color. The output is itself is just a constant color, a shader of yellow here.

The complete program is:

<shader compiler="xmlshader" name="simpleshader">
  <technique priority="100">
    <pass>
      <vp plugin="glcg">
        <cgvp>
          <program>
          <![CDATA[
          float4 main (float4 Position : POSITION,
                  uniform float4x4 ModelViewProj : state.matrix.mvp) : POSITION
          {
                  return mul (ModelViewProj, Position);
          }
          ]]>
          </program>
        </cgvp>
      </vp>
      <fp plugin="glcg">
        <cgfp>
          <program>
          <![CDATA[
          float4 main () : COLOR
          {
                  return float4 (1, 0.8, 0, 1);
          }
          ]]>
          </program>
        </cgfp>
      </fp>
    </pass>
  </technique>
</shader>

And the result when running shadertest:

crystal000.png

Adding Texture Mapping

Drawing a solid colored mesh is not exactly exciting. As the next step, the shader shall get its output color from a texture.

The changed Cg code can be inspected in shadertest_2.xml. Notable is that the programs need additional inputs: the vertex program requires texture coordinates, the fragment program takes a texture as input. These need some additional markup to assign bindings between CS resources and the shader program inputs. [2]

[2]A third change is that the texture coordinate has to be passed from the vertex to the fragment program. However, this does not affect or require any settings in the bindings part of the shader.

The two new data, texture coordinates and texture, are of different nature and are thus bound in different ways.

The texture coordinates are provided by the mesh in a render buffer. Any per-vertex data is stored in render buffers in Crystal Space. Other buffers you are likely to see are for vertex normals, tangents and bitangents, vertex colors, or additional texture coordinate sets. [3]

[3]In fact, even the object space position is delivered in a render buffer. However, it is implicitly bound and thus does not need any special binding markup.

To bind a buffer, a buffer element is used:

<buffer source="texture coordinate" destination="IN.texCoord" />

The name attribute identifies the buffer. It can be one of a set of predefined names (such as "texture coordinate", "normal", "color") or an arbitrary name. The destination attribute specifies the target of the binding. The possible values are actually dependent on the type of vertex program used. For Cg programs you can use actual variable identifiers as destinations.

To bind a texture, a texture element is used:

<texture name="tex diffuse" destination="textureDiffuse" />

Similarly, the name attribute identifies the texture. There is no set of predefined names, however, there are some "common" names which, by convention, should have the same meaning everywhere in the world of Crystal Space. "tex diffuse" is such a name and identifies the diffuse texture of a surface. Here the destination attribute specifies the target of the binding as well. Again, the possible values are actually dependent on the type of fragment program used, and with Cg programs you can use actual variable identifiers.

And the result when running shadertest:

crystal001.png

Adding Per-Vertex Lighting

As the next step, we will add lighting to the shader. Just some simple per-vertex lighting, nothing too fancy. The modified Cg code can be inspected in shadertest_3.xml.

Of the input data used you'll notice that some additional per-vertex data is used: the vertex normal. Binding that value to the Cg inputs is, as before, done with a buffer element.

However, you will also notice the light parameters are declared slightly differently, as uniform. These values are "uniform" for a single rendered mesh (thus they could also be called "constants", although, they are not constant in the strictest sense).

For a rendered mesh, Crystal Space will select a set of lights and will provide them to the shader in different shader variables. As with all information, a connection between the data provided by Crystal Space and the declaration in a shader program needs to be made. To that end, the variablemap element maps an arbitrary shader variable to a program input:

<variablemap variable="light count" destination="IN.numLights" />

The name attribute identifies the shader variable. There are some predefined variables which contain information provided by Crystal Space [4]. But in general, shader variables can be arbitrarily named. Commonly, a shader may use some custom shader variable to provide a parameter to the effect which is then set in a material using the shader. E.g. the "specular" parameter that usually sets the material's specular color. Here the destination attribute specifies the target of the bindinshader variable value. The possible values are actually dependent on the type of shader program used. Wnd with Cg programs you can use actual variable identifiers.

[4]http://www.crystalspace3d.org/docs/online/manual/Shader-Variables.html#0 contains a list with engine-provided shader variables.

Note that some of the variables used as a target are arrays. On the Crystal Space side, most light information is passed as a shader variable array. The mapping between array items in the Cg side and array items on the Crystal Space sides happens automatically. If you would want to use a single element of a shader variable array you would use typical array element syntax in name, e.g. light diffuse[0].

Another subtle but important change is the lights attribute to the shader element. I tells Crystal Space how many lights a shader can handle - this information is used to (possibly) render a mesh in multiple passes.

And the result when running shadertest:

crystal002.png

Adding Ambient Lighting

Crystal Space provides the ambient light color in a shader variable light ambient. To apply ambient lighting simply add the value of this shader variable to the computed diffuse lighting. The modified Cg code can be inspected in shadertest_4.xml.

And the result when running shadertest:

crystal003.png

If you compare it to the previous image you can see the teapot on this one is slightly brighter.

Making the Shader Support Multiple Passes

As mentioned before, Crystal Space can render a mesh in multiple passes when more lights are affecting a mesh than the shader has support for. To test this, change the lights attribute to one (shadertest_5.xml). The result:

crystal003.png

But wait. These are not all lights! Where are the others?

The reason lies in the way the shader output is blended with the framebuffer. Or rather, how it is not, as the default behaviour is just overwriting the last color. So for this shader, the lighting for the last light computed overrides all lights before.

The solution is to use additive blending:

<mixmode><add /></mixmode>

However, this is incorrect for the first pass. The first pass needs to stay a simple "overwrite" blending. But how to toggle the blending for different passes?

The easiest way is to use Crystal Space's facility to create multiple variations out of a single shader source, the shader conditions. These are checks such as "does a shader variable exists", "does a shader variable have a certain type", "does a shader variable have a certain value", which in itself can be combined with logical operations. The conditions are specified inside XML processing instructions.

"Overwrite" blending should be applied in the very first pass rendered. This is the case if two conditions are met: first, the number of the light rendering pass (passed in the shader variable pass number) is 0; second, the shader variable pass do_ambient, generally specified in the render layers setup, is present. Otherwise, additive blending should be used. This translates to the following Crystal Space shader condition:

<?if (vars."pass number".int == 0) && (vars."pass do_ambient") ?>
<mixmode><add /></mixmode>
<?endif?>

The complete source is in shadertest_6.xml, the result is:

crystal005.png

However, if you compare it with the result image from Adding Ambient Lighting you will notice that the latest result is lighter, This is because the ambient contribution is now added multiple times, specifically, once with each pass. The solution is to only add ambient in the first pass as well. To achieve this use the same condition used to change the mixmode, only in the Cg code now:

...
]]>
        <?if !((vars."pass number".int == 0) && (vars."pass do_ambient")) ?>
        OUT.litColor = lightingDiffuse;
        <?else?>
        OUT.litColor = lightingDiffuse + IN.ambient;
        <?endif?>
<![CDATA[
...

Notice that the conditions are outside the CDATA block of the other source code. This is needed as otherwise the conditions would not be recognized as such but be treated as part of the Cg source code, which would cause a Cg compile error.

The complete source is in shadertest_7.xml, the result is:

crystal006.png

Further Reading

The Shader 101 tutorial on the Crystal Space home page is a tutorial creating a shader rooted in a practical problem.

« back