Chapter 5. Extending the shaders with plugs

The core idea of our approach is that the base shader defines points where calls to user-defined functions may be inserted. We call these places plugs, as they act like sockets where logic may be added. Each plug has a name and a given set of parameters. The effects can use special function names, starting with PLUG_ and followed by the plug name. These declarations will be found and the renderer will insert appropriate calls to them from the base shader.

A trivial example of an effect that makes colors two times brighter is below. This is a complete X3D file, so you can save it as test.x3dv and open with any tool supporting our extensions, like view3dscene.

#X3D V3.2 utf8
PROFILE Interchange
Shape {
  appearance Appearance {
    material Material { }
    effects Effect {
      language "GLSL"
      parts EffectPart {
        type "FRAGMENT"
        url "data:text/plain,
        void PLUG_fragment_modify(
          inout vec4 fragment_color)
        {
          fragment_color.rgb *= 2.0;
        }"
      }
    }
  }
  geometry Sphere { }
}

Our extensions to X3D are marked with the bold font in the example above. The GLSL code inside our extensions is marked with the italic font. The special GLSL function name PLUG_fragment_modify indicates that we use the fragment_modify plug. This particular plug is called after calculating everything else for this pixel (textures, lighting) and allows to intuitively modify the final pixel color. fragment_color is an inout parameter, by modifying it we modify the color that will be displayed on the screen.

A reference of all the plugs available in our implementation is at the end of this paper, see Appendix A, Reference of available plugs. For each plug, like this PLUG_fragment_modify, we define a list of parameters and when it is called.

Many usage scenarios are possible:

  1. The Effect nodes may use plug names defined inside the renderer internal shaders. This is the most usual case. It allows the authors to extend or override a particular shading parameter.

  2. The Effect nodes may also use the plug names defined in the previous Effect nodes on the same shape. It is trivially easy (just add a magic comment) to define plugs in your own shader code. This way your own effects can be customized.

  3. Inside the renderer implementation, the same approach can be used to implement some internal effects. We have reimplemented many internal effects of our engine, like the fog, shadow maps (see our shadow mapping extensions for X3D [X3D Shadow Maps]) and the bump mapping to use our plugs approach. This made their implementation very clean, short and nicely separated from each other. It also proves that the authors have the power to implement similar effects easily by themselves.

Actually, there are even more possibilities. We have been talking above about the renderer internal shaders, but the truth is a little more flexible. When you place a standard shader node (like a ComposedShader node for GLSL shaders) on the Appearance.shaders list, then it replaces the internal renderer shaders. If you define the same (or compatible) plugs inside your shader, then the internal renderer effects are even added to your own shader. Of course user effects are added to your shader too. This way even the standard X3D shader nodes become more flexible. Note that if you do not define any plugs inside your ComposedShader node, it continues to function as before — no effects will be added.

5.1. Effect node

New Effect node holds information about the source code and uniform values specific to a given effect. The node specification below follows the style of the X3D specification [X3D].

Effect : X3DChildNode

SFString [] language ""
  # Language like "GLSL", "CG", "HLSL".
  # This effect will be used
  # only when the base renderer shader
  # uses the same language.

SFBool [in,out] enabled TRUE
  # Easily turn on/off the effect.
  # You could also remove/add the node
  # from the scene, but often toggling
  # this field is easier for scripts.

MFNode [] parts [] # EffectPart
  # Source code of the effect.

# A number of uniform values may also be
# declared inside this node.

Inside the Effect node a number of uniform values may be defined, passing any X3D value to the shader. Examples include passing current world time or a particular texture to the shader. Uniform values are declared exactly like described in the standard X3D Programmable shaders component [X3D Shaders].

The effect source code is split into a number of parts:

EffectPart : X3DNode, X3DUrlObject

SFString [] type "VERTEX"
  # Like ShaderPart.type:
  # allowed values are
  # VERTEX | GEOMETRY | FRAGMENT.

MFString [in,out] url []
  # The source code, like ShaderPart.url.
  # May come from an external file (url),
  # or inline (following "data:text/plain,").
  # In XML encoding, may also be inlined in CDATA.

Inside the effect part source code, the functions that enhance standard shaders behavior are recognized by names starting with PLUG_. Of course other functions can also be defined and used. Uniform variables can be passed to the effect, also varying variables can be passed between the vertex and fragment parts, just like with standard shader nodes.

In a single EffectPart node, many PLUG_ functions may be declared. However, all plug functions must be declared in the appropriate effect type. For example, the fragment_modify plug cannot be used within a VERTEX shader. If the effect requires some processing per-vertex and some per-fragment, it is necessary to use two EffectPart nodes, with different types. This allows to implement our system for shading languages with separate namespaces for vertex and fragment parts (like GLSL). A single part may declare many variables and functions, but it must be completely contained within a given shader type.

Note that it is completely reasonable to have an EffectPart node with source code that does not define any PLUG_xxx functions. Such EffectPart node may be useful for defining shading language utility functions, used by other effect parts.

For shading languages that have separate compilation units (like the OpenGL Shading Language) the implementation may choose to place each effect part in such separate unit. This forces the shader code to be cleaner, as you cannot use undeclared functions and variables from other parts. It also allows for cleaner error detection (parsing errors will be detected inside the given unit).