Chapter 6. Extensions for geometry shaders

Geometry shaders are an optional stage of 3D rendering. A geometry shader program is executed once for each primitive, like a single triangle. It works between the vertex and fragment shader — it knows all the outputs from the vertex shader, and is responsible for passing them to the rasterizer. Geometry shader can use the information about given primitive (vertex positions and attributes) and can generate other primitives. A single geometry shader may generate any number of primitives, so it is possible to explode a single input primitive into a number of others. Or we can entirely discard some primitives. The type of the primitive may be changed by the geometry shader — for example, triangles may be converted into points or the other way around.

Figure 6.1. Converting mesh into a dense line and point sets by geometry shaders

Converting mesh into a dense line and point sets by geometry shaders

More details about using the geometry shaders with X3D and our engine are on http://castle-engine.sourceforge.net/x3d_implementation_shaders.php#section_geometry. The examples there show how to use the standard ComposedShader node to define a geometry shader for the shape. Here, we investigate how our effects improve the geometry shaders.

6.1. Robust geometry shaders

When we write a geometry shader, we want to control the logic of the primitive generation and the output primitive type. This means that the shader author wants to provide the main part of the geometry shader, that contains the main() entry point and necessary layout declarations.

We want to enable integration of our effects with user geometry shaders. It must be possible to write a flexible geometry shader code, that works with any internal effects and user effects in Effect nodes. In other words, when writing the geometry shaders, we do not want to hardcode what values have to be passed from vertex processor to the rasterizer. To make this possible, with every geometry shader we will link additional code with three functions:

  • void geometryVertexSet(const int index) — set output vertex to be equal to the input vertex of the given index.

  • void geometryVertexZero() — set all output vertex attributes to zero. This is really useful only before doing a series of geometryVertexAdd calls.

  • void geometryVertexAdd(const int index, const float scale) — add to the output vertex given input vertex, scaled.

The idea is that geometryVertexSet can be used to simply pass-through values from vertex processing to the rasterizer. Calling geometryVertexSet(i) is equivalent (but possibly more efficient) to

geometryVertexZero();
geometryVertexAdd(i, 1.0);

For example, this is a trivial pass-through geometry shader, that doesn't do anything. Thanks to using geometryVertexSet, every other effect still works (without geometryVertexSet, various effects would break, because values would not be passed from vertex shaders to fragment shaders). This is what we mean by robust geometry shaders.

effects Effect {
  language "GLSL"
  parts EffectPart {
    type "GEOMETRY"
    url "data:text/plain,
      #version 150

      layout(triangles) in;
      layout(triangle_strip, max_vertices = 3) out;

      void geometryVertexSet(const int index);

      void main()
      {
        for(int i = 0; i < gl_in.length(); i++)
        {
          gl_Position = gl_in[i].gl_Position;
          geometryVertexSet(i);
          EmitVertex();
        }
        EndPrimitive();
      }"
  }
}

For more elaborate cases, geometryVertexAdd may be used to blend many input vertexes into one final output vertex. An example below shows a geometry shader that replaces every triangle with an averaged single point.

effects Effect {
  language "GLSL"
  parts EffectPart {
    type "GEOMETRY"
    url "data:text/plain,
      #version 150

      layout(triangles) in;
      layout(points, max_vertices = 1) out;

      void geometryVertexZero();
      void geometryVertexAdd(const int index, const float scale);
      void main()
      {
        gl_Position = (
          gl_in[0].gl_Position +
          gl_in[1].gl_Position +
          gl_in[2].gl_Position ) / 3.0;
        geometryVertexZero();
        geometryVertexAdd(0, 1.0 / 3.0);
        geometryVertexAdd(1, 1.0 / 3.0);
        geometryVertexAdd(2, 1.0 / 3.0);
        EmitVertex();
        EndPrimitive();
      }"
  }
}

The geometryVertexXxx functions magically take into account all the attributes that need to be handled for renderer internal effects. User effects (Effect nodes) may have to override corresponding geometry_vertex_xxx plugs to also be automatically handled this way.