Jump to content

  • Log In with Google      Sign In   
  • Create Account


Like
3Likes
Dislike

GLSL 4.0: Using Subroutines to Select Shader Functionality

By David Wolff | Published Feb 02 2012 08:38 PM in OpenGL

If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

In GLSL, a subroutine is a mechanism for binding a function call to one of a set of possible function definitions based on the value of a variable. Subroutines therefore provide a way to select alternate implementations at runtime without swapping shader programs and/or recompiling, or using if statements along with a uniform variable.

In this article by David Wolff, author of OpenGL 4.0 Shading Language Cookbook, we'll demonstrate the use of subroutines by rendering a teapot twice. The first teapot will be rendered with the full ADS shading model described earlier. The second teapot will be rendered with diffuse shading only. A subroutine uniform will be used to choose between the two shading techniques.

In many ways it is similar to function pointers in C. A uniform variable serves as the pointer and is used to invoke the function. The value of this variable can be set from the OpenGL side, thereby binding it to one of a few possible definitions. The subroutine's function definitions need not have the same name, but must have the same number and type of parameters and the same return type.

A single shader could be written to provide several shading algorithms intended for use on different objects within the scene. When rendering the scene, rather than swapping shader programs (or using a conditional statement), we can simply change the subroutine's uniform variable to choose the appropriate shading algorithm as each object is rendered.

Since performance is crucial in shader programs, avoiding a conditional statement or a shader swap can be very valuable. With subroutines, we can implement the functionality of a conditional statement or shader swap without the computational overhead.

In the following image, we see an example of a rendering that was created using subroutines. The teapot on the left is rendered with the full ADS shading model, and the teapot on the right is rendered with diffuse shading only. A subroutine is used to switch between shader functionality.

Posted Image


Getting ready
As with previous recipes, provide the vertex position at attribute location 0 and the vertex normal at attribute location 1. Uniform variables for all of the ADS coefficients should be set from the OpenGL side, as well as the light position and the standard matrices.

We'll assume that, in the OpenGL application, the variable programHandle contains the handle to the shader program object.

How to do it...
To create a shader program that uses a subroutine to switch between pure-diffuse and ADS shading, use the following code:

Use the following code for the vertex shader:
#version 400

subroutine vec3 shadeModelType( vec4 position, vec3 normal);

subroutine uniform shadeModelType shadeModel;

layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexNormal;

out vec3 LightIntensity;

struct LightInfo {
  vec4 Position; // Light position in eye coords.
  vec3 La; // Ambient light intensity
  vec3 Ld; // Diffuse light intensity
  vec3 Ls; // Specular light intensity
};
uniform LightInfo Light;

struct MaterialInfo {
  vec3 Ka; // Ambient reflectivity
  vec3 Kd; // Diffuse reflectivity
  vec3 Ks; // Specular reflectivity
  float Shininess; // Specular shininess factor
};
uniform MaterialInfo Material;

uniform mat4 ModelViewMatrix;
uniform mat3 NormalMatrix;
uniform mat4 ProjectionMatrix;
uniform mat4 MVP;

void getEyeSpace( out vec3 norm, out vec4 position )
{
  norm = normalize( NormalMatrix * VertexNormal);
  position = ModelViewMatrix * vec4(VertexPosition,1.0);
}

subroutine( shadeModelType )

vec3 phongModel( vec4 position, vec3 norm )
{
  // The ADS shading calculations go here (see: "Using
  // functions in shaders," and "Implementing
  // per-vertex ambient, diffuse and specular (ADS) shading")
  ...
}

subroutine( shadeModelType )

vec3 diffuseOnly( vec4 position, vec3 norm )
{
  vec3 s = normalize( vec3(Light.Position - position) );
  return
	  Light.Ld * Material.Kd * max( dot(s, norm), 0.0 );
}

void main()
{
  vec3 eyeNorm;
  vec4 eyePosition;

  // Get the position and normal in eye space

  getEyeSpace(eyeNorm, eyePosition);

  // Evaluate the shading equation. This will call one of
  // the functions: diffuseOnly or phongModel.
  LightIntensity = shadeModel( eyePosition, eyeNorm );

  gl_Position = MVP * vec4(VertexPosition,1.0);
}

Use the following code for the fragment shader:
#version 400

in vec3 LightIntensity;

layout( location = 0 ) out vec4 FragColor;

void main() {
FragColor = vec4(LightIntensity, 1.0);
}

In the OpenGL application, compile and link the above shaders into a shader program, and install the program into the OpenGL pipeline.

Within the render function of the OpenGL application, use the following code:
GLuint adsIndex =
glGetSubroutineIndex( programHandle,
					 GL_VERTEX_SHADER,"phongModel" );
GLuint diffuseIndex =
  glGetSubroutineIndex(programHandle,
					   GL_VERTEX_SHADER, "diffuseOnly");

glUniformSubroutinesuiv( GL_VERTEX_SHADER, 1, &adsIndex);
... // Render the left teapot

glUniformSubroutinesuiv( GL_VERTEX_SHADER, 1, &diffuseIndex);
... // Render the right teapot

How it works...
In this example, the subroutine is defined within the vertex shader. The first step involves declaring the subroutine type.

subroutine vec3 shadeModelType( vec4 position,
					 vec3 normal);

This defines a new subroutine type with the name shadeModelType. The syntax is very similar to a function prototype, in that it defines a name, a parameter list, and a return type. As with function prototypes, the parameter names are optional.

After creating the new subroutine type, we declare a uniform variable of that type named shadeModel.

subroutine uniform shadeModelType shadeModel;

This variable serves as our function pointer and will be assigned to one of the two possible functions in the OpenGL application.

We declare two functions to be part of the subroutine by prefixing their definition with the subroutine qualifier:

subroutine ( shadeModelType )

This indicates that the function matches the subroutine type, and therefore its header must match the one in the subroutine type definition. We use this prefix for the definition of the functions phongModel and diffuseOnly. The diffuseOnly function computes the diffuse shading equation, and the phongModel function computes the complete ADS shading equation.

We call one of the two subroutine functions by utilizing the subroutine uniform shadeModel within the main function.

LightIntensity = shadeModel( eyePosition, eyeNorm );

Again, this call will be bound to one of the two functions depending on the value of the subroutine uniform shadeModel, which we will set within the OpenGL application.

Within the render function of the OpenGL application, we assign a value to the subroutine uniform with the following steps. First, we query for the index of each subroutine function using glGetSubroutineIndex. The first argument is the program handle. The second is the shader stage. In this case, the subroutine is defined within the vertex shader, so we use GL_VERTEX_SHADER here. The third argument is the name of the subroutine. We query for each function individually and store the indexes in the variables adsIndex and diffuseIndex.

To select the appropriate subroutine function, we need to set the value of the subroutine uniform shadeModel. To do so, we call glUniformSubroutinesuiv. This function is designed for setting multiple subroutine uniforms at once. In our case, of course, we are setting only a single uniform. The first argument is the shader stage (GL_VERTEX_SHADER), the second is the number of uniforms being set, and the third is a pointer to an array of subroutine function indexes. Since we are setting a single uniform, we simply provide the address of the GLuint variable containing the index, rather than a true array of values. Of course, we would use an array if multiple uniforms were being set. In general, the array of values provided as the third argument is assigned to subroutine uniform variables in the following way. The ith element of the array is assigned to the subroutine uniform variable with index i. Since we have provided only a single value, we are setting the subroutine uniform at index zero.

You may be wondering, "How do we know that our subroutine uniform is located at index zero? We didn't query for the index before calling glUniformSubroutinesuiv!" The reason that this code works is that we are relying on the fact that OpenGL will always number the indexes of the subroutines consecutively starting at zero. If we had multiple subroutine uniforms, we could (and should) query for their indexes using glGetSubroutineUniformLocation, and then order our array appropriately.

Finally, we select the phongModel function by setting the uniform to adsIndex and then render the left teapot. We then select the diffuseOnly function by setting the uniform to diffuseIndex and render the right teapot.

There's more...
A subroutine function defined in a shader can match multiple subroutine types. In that case, the subroutine qualifier can contain a comma-separated list of subroutine types. For example, if a subroutine matched the types type1 and type2, we could use the following qualifier:

subroutine( type1, type2 )

This would allow us to use subroutine uniforms of differing types to refer to the same subroutine function.

Summary
With subroutines we can implement the functionality of a conditional statement or shader swap without the computational overhead.





Comments
I think the "new" subroutine technique is the right way to go when you need complex shader management like in high end engines. Bad that many of them not use OpenGL and bad that OpenGL 4.0 is currently not standard on PC... Today you need to much backward compability...
My engine will go this way... hopefully when I have a better work environment :)

In summary: Great article about some of the reasons why OpenGL is not old school. Looking forward!

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS