Chapter 9. Implementation

We have implemented everything described in this paper in our open-source (LGPL) 3D game engine, see [Castle Game Engine].

Our current implementation supports only one shading language: GLSL, the OpenGL Shading Language. As our engine is cross-platform and focused on OpenGL, this is the most natural shading language for us. However, we have designed our extensions to be applicable to other shading languages (like Cg and HLSL) as well.

One notable concept of GLSL is the separate compilation units. It means that a code can be split into many units which are parsed and compiled separately, and only linked together. This allows to write cleaner shader code (you cannot use undeclared functions from other shader parts). It also naturally matches with our effects definition, as each EffectPart becomes simply one compilation unit.

A similar feature is found in many other programming languages, under the names of units or modules. But it is not available in two other popular shading languages: Cg and HLSL. To make sure that our idea is applicable to these shading languages, we have explicitly tested that even without the separate compilation units, our implementation still works without problems.

9.1. Outline of the implementation

We present a pseudo-code that generates a complete shader source for rendering a given shape. It takes into account standard rendering features (X3D light sources, textures and such), custom shaders (by X3D nodes like ComposedShader) and our shader effects (by Effect nodes). All effects are properly combined to form the final shader code.

We keep the final shader code as three arrays of strings. Each array keeps code for a specific shader type: vertex, geometry and fragment. Each string corresponds to a compilation unit for GLSL, for other shading languages the strings can be just concatenated at the end. To make the plugs actually work, we add calls to their functions. We also add external function declarations at appropriate places. For languages other than GLSL, they will simply become forward function declarations when all the parts are concatenated.

9.1.1. Helper functions

First define a function Plug. It is responsible for actual text processing that makes the plug functions correctly called. The argument PlugValue is scanned for all PLUG_xxx function definitions.

The argument CompleteCode is searched for the matching /* PLUG: xxx ... */ comments. The final shader (for given shape) in FinalShader is also searched for the /* PLUG: xxx ... */ comments. In practice, CompleteCode given here is either the FinalShader or the shader for a specific texture or light source.

Appropriate calls and forward declarations are inserted to the CompleteCode. In the process, all handled PLUG_xxx functions inside PlugValue are also renamed to unique names, since a single plug may be overridden by many effects. The modified PlugValue is inserted to the CompleteCode as well. Effectively, the caller can usually forget about the PlugValue afterwards — it has been processed, and correctly merged with the CompleteCode.

type
  TShaderType = (vertex, geometry, fragment);
  TShaderSource = array [TShaderType] of a string list;

var
  { shader for the whole shape }
  FinalShader: TShaderSource;

procedure Plug(
  EffectPartType: TShaderType;
  PlugValue: string;
  CompleteCode: TShaderSource);

var
  PlugName, ProcedureName, PlugForwardDeclaration: string;

  { Look for /* PLUG: PlugName (...) */ inside
    given CodeForPlugDeclaration.
    Return if any occurrence found. }
  function LookForPlugDeclaration(
    CodeForPlugDeclaration: string list): boolean;
  begin
    Result := false
    for each S: string in CodeForPlugDeclaration do
    begin
      AnyOccurrencesHere := false
      while we can find an occurrence
        of /* PLUG: PlugName (...) */ inside S do
      begin
        insert into S a call to ProcedureName,
        with parameters specified inside the /* PLUG: PlugName (...) */,
        right before the place where we found /* PLUG: PlugName (...) */

        AnyOccurrencesHere := true
        Result := true
      end

      if AnyOccurrencesHere then
        insert the PlugForwardDeclaration into S,
        at the place of /* PLUG-DECLARATIONS */ inside
        (or at the beginning, if no /* PLUG-DECLARATIONS */)
    end
  end

var
  Code: string list;
begin
  Code := CompleteCode[EffectPartType]

  HasGeometryMain := HasGeometryMain or
    ( EffectPartType = geometry and
      PlugValue contains 'main()' );

  while we can find PLUG_xxx inside PlugValue do
  begin
    PlugName := the plug name we found, the "xxx" inside PLUG_xxx
    PlugDeclaredParameters := parameters declared at PLUG_xxx function

    { Rename found PLUG_xxx to something unique. }
    ProcedureName := generate new unique procedure name,
    for example take 'plugged_' + some unique integer

    replace inside PlugValue all occurrences of 'PLUG_' + PlugName
    with ProcedureName

    PlugForwardDeclaration := 'void ' + ProcedureName +
    PlugDeclaredParameters + ';' + newline

    AnyOccurrences := LookForPlugDeclaration(Code)

    { If the plug declaration is not found in Code, then try to find it
      in the final shader. This happens if Code is special for given
      light/texture effect, but PLUG_xxx is not special to the
      light/texture effect (it is applicable to the whole shape as well).
      For example, using PLUG_vertex_object_space inside
      the X3DTextureNode.effects. }
    if not AnyOccurrences and
       Code <> Source[EffectPartType] then
      AnyOccurrences := LookForPlugDeclaration(Source[EffectPartType])

    if not AnyOccurrences then
      Warning('Plug name ' + PlugName + ' not declared')
  end

  { regardless if any (and how many) plug points were found,
    always insert PlugValue into Code. This way EffectPart with a library
    of utility functions (no PLUG_xxx inside) also works. }
  Code.Add(PlugValue)
end

Using the Plug function, we can create the EnableEffects function. It handles the effects list, correctly processing it and adding to the given CompleteCode..

procedure EnableEffects(
  Effects: list of Effect nodes;
  CompleteCode: TShaderSource);
begin
  for each Effect in Effects do
    if Effect.enabled and
       Effect.language matches renderer shader language then
      for each EffectPart in Effect.parts do
        Plug(EffectPart.type, GetUrl(EffectPart.url), CompleteCode)
end

9.1.2. Final algorithm

Using the above functions, we construct the final shader code for given shape. All the effects (including effects specific to lights and textures) are correctly applied by the algorithm below. Specific requirements of the geometry shaders are also taken into account.

FinalShader := new TShaderSource
set FinalShader to basic rendering code
HasGeometryMain := false

At the beginning, FinalShader it set to a simple code that renders 3D object with no lights, no textures and no effects. The code contains magic /* PLUG: xxx ... */ comments, which will allow to enhance it in the following steps.

The real GLSL shader code used by our engine at this step may be found in our engine sources. See http://svn.code.sf.net/p/castle-engine/code/trunk/castle_game_engine/src/x3d/opengl/glsl/template.vs for the vertex shader, http://svn.code.sf.net/p/castle-engine/code/trunk/castle_game_engine/src/x3d/opengl/glsl/template.fs for the fragment shader and http://svn.code.sf.net/p/castle-engine/code/trunk/castle_game_engine/src/x3d/opengl/glsl/template.gs for the geometry shader.

Note that our default geometry shader contains only a set of utility functions, without a main() entry. It will be discarded later if no geometry shader code with a main() definition will be found in the user shaders. That is, if HasGeometryMain will remain false. See Chapter 6, Extensions for geometry shaders for reasons of this behavior.

if a complete custom shader code is provided then
  FinalShader[fragment] := empty
  FinalShader[vertex] := empty
  FinalShader := FinalShader + custom shader code
  HasGeometryMain := custom shader code contains some "GEOMETRY" shader

The X3D file may contain shader code that should replace the default shaders, following [X3D Shaders] specification. For example, a ComposedShader node may be present with a complete GLSL code. We use it at this step.

This step trivially allows the ComposedShader code to also contain plug declarations, like /* PLUG: xxx ... */. The same plug names as our default names may be used (like fragment_modify and so on), in which case the same user effects will be useful with the custom shader. This even allows the browser to add some internal effects (like shadow maps) to the custom shader template.

Alternatively, the ComposedShader may have a completely different approach to rendering. Then it may expose a completely different set of plug names, reflecting a different set of parameters to control.

Note again the special treatment of geometry shaders. Our default geometry shader code (which should contain our utility functions) is always kept. We also update HasGeometryMain.

for each Light in shape.Lights do
  LightShader := new TShaderSource
  1 set LightShader to basic lighting code (optimized for this Light)
  2 EnableEffects(Light.Effects, LightShader)
  3 LightContribution := LightShader.ExtractFirst
  4 Plug(fragment, LightContribution, FinalShader)
  5 FinalShader := FinalShader + LightShader

A little care is needed to correctly add light sources. Remember that lights may have user-defined effects that should be applied only to the specific light source. That's why we temporarily keep the shader code specific to a given light source in a separate LightShader variable.

1

The light source contribution will be linked with the final shader also using our plugs. We initially add to LightShader a function called PLUG_add_light_contribution_side that takes care of calculating light contribution, following normal X3D light equations. This function should be optimized for the given light type (spot, directional, point) and light parameters (for example, lights without an attenuation factor or an infinite radius or zero specular term may be optimized at this point). If we want Phong shading, this function should be added as the first string of the LightShader[fragment]. If we want Gouraud shading, it should go to LightShader[vertex] instead.

The actual initial GLSL light shader code used by our engine at this step may be found in our engine sources, see http://svn.code.sf.net/p/castle-engine/code/trunk/castle_game_engine/src/x3d/opengl/glsl/template_add_light.glsl.

2

Next we apply Effect nodes specific to this light source. After this, LightShader contains the initial code merged with user effects. Doing it this way means that the standard light source plugs (like light_scale) as well as custom plugs (defined in one Effect node and used by following Effect nodes) work correctly.

3

After applying user effects, we extract (get and delete) from LightShader our initial code. It may be modified now, since calls to user effects are now present inside.

4

The extracted LightContribution must now be connected with the FinalShader code. This can be done by a simple call to the Plug function, which will notice the PLUG_­add_­light_­contribution_­side present inside LightContribution. After this operation, everything is connected: final shader calls PLUG_­add_­light_­contribution_­side, which in turn calls user effects on the light source.

5

Finally, add the remaining code to be linked together with the FinalShader. This step simply adds the strings from one list to the other, with no processing.

for each Texture in shape.Textures do
  TextureShader := new TShaderSource
  set TextureShader to basic code (optimized for this Texture)
  EnableEffects(Texture.Effects, TextureShader)
  TextureApplication := TextureShader.ExtractFirst
  Plug(fragment, TextureApplication, FinalShader)
  FinalShader := FinalShader + TextureShader

Texture effects require a similar approach as light effects, to correctly catch all the ways how plugs may be used.

We start by creating a default shader code that applies the texture, knowing the texture type (2D, 3D, cube map), texture mode (multiply, add and such) and other properties. It should follow all X3D texturing and multi-texturing requirements. The code should define a function named PLUG_main_texture_apply that can be later connected to the final shader. It should also declare plug named texture_color, that can be used by user effects for this texture.

There are actually more differences between the application of the light and texture effects. Section 9.2, “Correct shadows from multiple light sources” describes one feature that makes their logic slightly more complicated.

EnableEffects(appearance node.Effects, FinalShader)

for each group node containing this shape do
  EnableEffects(group node.Effects, FinalShader)

Effects specific to a given shape, and effects for all groups containing this shape, are applied.

The effects at this point may override also lights and textures plugs, like light_scale. That's simply because we have already added all the lighting and texturing shading code to the FinalShader. Overriding light_scale at this point means that we scale the contribution of every light by the same function.

if HasGeometryMain then
  for each FragmentPart in FinalShader[fragment] do
    FragmentPart := '#define HAS_GEOMETRY_SHADER' + FragmentPart else
  FinalShader[geometry] := empty

Decide if we really want to link geometry shaders, based on whether we have main() for geometry shaders. If yes, then HAS_GEOMETRY_SHADER symbol has to be defined, as it may be useful for fragment shader authors. Otherwise, discard all geometry shader code.

At the end, FinalShader is just a collection of strings forming a shading language source code. For GLSL, each string is naturally a separate compilation unit, and can be compiled in isolation. For other shading languages, the parts may be simply concatenated together as necessary.

The full, actual source code of this operation is available in our engine sources, see the unit GLRendererShader. Source code is on http://svn.code.sf.net/p/castle-engine/code/trunk/castle_game_engine/src/x3d/opengl/glrenderershader.pas.