Yo Frankie! | Crystal Space Tutorials

Crystal Space Shader Weaver

The Crystal Space "shader weaver" generates Crystal Space shaders out of a graph. The weaver has some convenience functions and features, such as automatic conversion of coordinates and normals between spaces and efficient culling of vertex-to-fragment data which is not required, and has a very modular approach to describing shaders, simplifying the creation of complex shaders.


It is recommended to read the "Introduction to Crystal Space Shaders": the weaver system is working atop the classic XML shader system; some elements are used verbatim (e.g. bindings), some concepts are used in a similar fashion (e.g. techniques).

Basic Concept

The initial motivation for creation of came from so-called Abstract Shade Trees wherein shaders are described by a tree structure. But unlike "classic" trees, where nodes where operations such as "dot product" or "read texture", the node operations are relatively high-level. There are only simple connections between individual nodes and the compiler figures out which inputs and outputs to match based on their types.

The Crystal Space shader weaver provides the essential feature of Abstract Shader Trees, the input/output matching and type conversion. However, out of practical necessity, inputs and outputs can also be manually connected. Effectively this makes it possible to specify shaders both in a high- and low-level fashion, solely depending on whether more or less complex nodes are used in the graph.

Nomenclature and Structure

A "weaver shader" consists, like an XML shader, out of multiple techniques and passes. However, the format inside the passes is vastly different.

The shader itself is described by a graph. The nodes are called snippets, links between snippet connections.

A snippet can either be a compound snippet or an atom snippet.

Compound snippets themselves can contain multiple techniques, each again containing a subgraph describing their behaviour.

Atom snippets contain actual shader program code emitted in the actual shader. An atom snippet consists of multiple blocks: putting a meaningful statement into a shader usually requires changes at multiple places (e.g. for a constant, you need to declare a variable the constant is assigned to, bind a shader variable to the program variable, optionally provide a default value, and finally read out the program variable and put it into a place known to the generator). This fact is accomodated by these "block"s. Each block contains a fragment of shader markup or shader program code, inserted at a place specified in the block definition.

Furthermore, atom snippets declare input and output values. Each can possess additional auxiliary information, called attributes. Inputs can optionally have default values.


The shader weaver is, conceptually, not tied to an underlying technology for writing shaders. The job of combining the different fragments of a shader program to a whole is delegated to other plugins, named "combiner". Two combiners are available: a "default" combiner for elements of shaders independent from the shader program(s) (such as buffer and texture bindings) and a user-set combiner responsible for generating the actual shader program(s).

In practice, the only user settable combiner available is one for Cg.

Input Resolution

Input to a weaver snippet is resolved as follows: first, the outputs of snippets immediately predecessing the current snippets are considered. An output is not considered twice for a node - ie when an output was already matched to another input of the current snippet it is not considered dor automatic matching again. When there is a direct match between the type of an input and an output, the matching output is used. Otherwise, a conversion from the type of the output and the input is attempted. If such a conversion exists, its "cost" is reported. When all outputs were checked, the one with the lowest cost is used. However, if no output can be converted, the search continues with all snippets predecessing the predecessor and so on. If no output is found in any of the checked snippets a user-specified default is used. If no default was given the input value is undefined.

"Output" Snippets

Any shader essentially needs to output two values: a coordinate in camera space, used by the hardware to determine the rasterization coordinates, and a color, used by the hardware to determine the color to raster.

The weaver looks for outputs for a color value and a (camera) position value, starting at all snippets which are not serving as input to other snippets. It scans the tree upwards until the needed output values are found. They are then used to provide a camera coordinate and a raster color for the hardware.

Basic Weaver Shader

As "classic" Crystal Space shaders, weaver shaders are written in XML. The outermost element must be shader. A name attribute must be present as well. The compiler attribute must have the value "shaderweaver", identifying the shader as handled by the shaderweaver plugin.

A weaver shader also consist of multiple techniques, each containing multiple passes.

The skeleton for a weaver shader is:

<shader compiler="shaderweaver" name="simpleweaver1">
  <technique priority="100">

Setting the combiner

In a pass node, the combiner(s) to be used when generating the shader must be specified. In case an atom snippet uses multiple combiners this selects which of the blocks targetted at the different combiners are used.

However, currently the only available combiner is the Cg one.

The syntax for setting a combiner is:

<combiner plugin="crystalspace.graphics3d.shader.combiner.glcg" />

Adding Minimal Nodes

As described in "Output" Snippets, we need to have at least a position output node and some color output node to get any result from the shader.

Among the snippet shipping with Crystal Space are of course ones for these basic tasks. The position snippet transforms the vertex position to camera space; the surface-classic snippet reads a color from the "diffuse" texture.

<snippet id="position" file="/shader/snippets/position.xml" />
<snippet id="surface" file="/shader/snippets/surface/surface-classic.xml" />

The file attribute identifies the snippet source file. The id attribute assigns each snippet a unique name: this is later needed for specifying connections between snippets.

The shader as a whole looks like this (simpleweaver1.xml):

<shader compiler="shaderweaver" name="simpleweaver1">
  <technique priority="100">
      <combiner plugin="crystalspace.graphics3d.shader.combiner.glcg" />

      <snippet id="position" file="/shader/snippets/position.xml" />
      <snippet id="surface" file="/shader/snippets/surface/surface-classic.xml" />

The result:


Adding Lighting

The stock snippets also contain snippets for lighting. These are split into two parts: first, a snippet for the lighting computation itself. These are realized in different techniques - presently, snippet for per-vertex and per-fragment lighting are available. The second part is the light application. This snippet basically takes various inputs - light diffuse color. surface diffuse color, light specular color, surface specular color, ambient color - and combines them into a single lit color value.

To include the necessary lighting snippets:

<snippet id="lighting" file="/shader/snippets/lighting/lighting-ppl.xml" />
<snippet id="apply_lighting" file="/shader/snippets/lighting/apply-lighting.xml" />

Notably the apply_lighting snippet has multiple inputs. To that end, connections between the snippets providing the data and apply_lighting snippet and the snippets providing the source data are needed. Currently, we have a diffuse surface color, provided by the surface snippet, and a light diffuse color, provided by the lighting snippet [1]. Also, the apply_lighting snippet has multiple inputs of the same type - thus explicit connections are used to disambiguate.

Inspect the contents of the snippet XML files to determine the available outputs and inputs. This leads to the following connections:

<connection from="position" to="lighting" />
<connection from="surface" to="apply_lighting">
  <explicit from="surfaceColor" to="surfaceDiffuse" />
<connection from="lighting" to="apply_lighting">
  <explicit from="diffuseColor" to="lightDiffuse" />

Each connection is specified by a connection element. The from and to attributes contain the IDs of the snippets to connect; the output values of the "from" snippets are connected with the input values of the "to" snippet.

The connection between "position" and "lighting" has no sub-elements - this means the automatic input/output matching is used.

The connections to "apply_lighting", on the other hand, have explicit children - these explicitly map outputs to inputs, avoiding ambiguities and resolving mistaken input/output matches performed by the weaver.

Finally, the "lighting" snippet needs the subset of lights to light as input. This is achieved with the following elements:

<parameter id="lightOffset" type="int">0</parameter>
<parameter id="maxLights" type="int">4</parameter>
<connection from="lightOffset" to="apply_lighting" />
<connection from="maxLights" to="apply_lighting" />

parameter values are simple constants or values sourced from shader variables that can be connected to snippets to serve as inputs. Here, two constants for the first light to handle [2] and the number of lights to handle are passed to the "apply_lighting" snippet.

Set the lights attribute of the shader node to 4.

[1]Actually, the lighting snippet also provides a specular color.
[2]Setting a different first light is useful when doing multiple lighting passes.

The complete shader is in simpleweaver2.xml. The result is:


Adding a Normal Map

The lighting snippet computes lighting per fragment [3], but it doesn't look like that. Using per-fragment normals would be nice. To that end, we merely need to add a snippet that provides normals from a normal map and connect that as input to the lighting snippet:


<snippet id="normalmap" file="/shader/snippets/surface/normalmap.xml" />

<connection from="position" to="normalmap" /> <connection from="normalmap" to="lighting" />

The complete shader is in simpleweaver3.xml. The result is:


In contrast, adding normal map support in a "classic" shader would have required manual bindings for normal, tangent and bitangent, normal map texture, and additional code to translate directions from or to tangent space and pushing some of that information from vertex to fragment program. To be honest, that is all still done - however, conveniently and reusably packaged into the normalmap.xml [4].

[3]The "ppl" in lighting-ppl.xml stands for - the slightly inaccurate - "per pixel lighting".
[4]Actually, multiple snippets working together, and generated shader code.

Adding More Advanced Effects

One of the benefits of using a shader weaver is the ability to make creation of complex effects easier. Say, for example, you want to blend the surface to a reflection of the environment, based on the fresnel factor for a point.

This can be done achieved with a couple more snippets and connections:

<snippet id="reflection" file="/shader/snippets/reflect/reflect-env-world.xml" />
<snippet id="reflection_cubemap" file="/shader/snippets/surface/envmap-cube.xml" />
<snippet id="fresnel" file="/shader/snippets/reflect/fresnel.xml" />
<snippet id="lerp_reflect_surface" file="/shader/snippets/lerp.xml" />

<connection from="position" to="reflection" />
<connection from="position" to="fresnel" />
<connection from="normalmap" to="reflection" />
<connection from="normalmap" to="fresnel" />
<connection from="reflection" to="reflection_cubemap" />
<connection from="reflection_cubemap" to="lerp_reflect_surface">
  <explicit from="envColor" to="b" />
<connection from="fresnel" to="lerp_reflect_surface" />
<connection from="apply_lighting" to="lerp_reflect_surface">
  <explicit from="result" to="a" />

The complete shader is in simpleweaver4.xml. The result is:

« back