Help end child hunger

GLSL Tutorial – Subroutines

 
Prev: Statements and Functions Next: Comm. App=>Shader
 

Subroutines are an OpenGL mechanism that allows the dynamic configuration of a shader’s behaviour without the need to rebuild the program.

Suppose we want to configure a shader that can provide a large range of behaviours. A typical scenario is lighting. We could write a single shader that provides several lighting algorithms, and through the use of uniform variables, control which algorithm to use each time. The reason to have such a setup may be performance related, i.e. in real time we could select the best algorithm that obtains at least a minimum predefined level of FPS.

Since we are only aiming at illustrating the usage of subroutines we are going to consider an absurdly basic example where we want to set the color to one of two possible values, either red or blue, depending on the value of a uniform variable. A simple shader to achieve this could be:

#version 400

layout (std140) uniform Matrices {
	mat4 pvm;
} ;

in vec4 position;

out vec4 color;

uniform int redBlueFlag;

void main()
{
	if (redBlueFlag == 1)
		color = vec4(1.0, 0.0, 0.0, 1.0);
	else
		color = vec4(0.0, 0.0, 1.0, 1.0);

	gl_Position = pvm * position ;
}

We could also use functions to set the color as follows:

#version 400

layout (std140) uniform Matrices {
	mat4 pvm;
} ;

in vec4 position;

out vec4 color;

uniform int redBlueFlag;

vec4 redColor() {

	return vec4(1.0, 0.0, 0.0, 1.0);
}

vec4 blueColor() {

	return vec4(0.0, 0.0, 1.0, 1.0);
}

void main()
{
	if (redBlueFlag == 1)
		color = redColor();
	else
		color = blueColor();

	gl_Position = pvm * position ;
}

Given the simplicity of the end results this certainly looks a little bit as overkill, but as mentioned before the aim is just to introduce the syntax for subroutines.

Enter subroutines. We are going to define a subroutine for each color option we have. First we must define a subroutine signature. In this case we have a function with no arguments that returns a vec4. Then we define the functions that we’re going to use with the defined signature.

// the signature
subroutine vec4 colorRedBlue ();

// option 1
subroutine (colorRedBlue ) vec4 redColor() {

	return vec4(1.0, 0.0, 0.0, 1.0);
} 

// option 2
subroutine (colorRedBlue ) vec4 blueColor() {

	return vec4(0.0, 0.0, 1.0, 1.0);
}

Note that when we defined the signature we did provide a name, colorRedBlue, this is the subroutine type name. When writing the subroutines we use the keyword subroutine and the type name, colorRedBlue, inside parenthesis, to indicate the type of the subroutine. Besides these details everything is identical to user defined functions.

We need one more thing, a special uniform variable to control which option will be used. This is declared as a subroutine uniform variable. This variable will be set in the application side as we’ll see afterwards.

subroutine uniform colorRedBlue myRedBlueSelection;

Now we can write our main function as follows:

void main()
{
	color = myRedBlueSelection();
	gl_Position = pvm * position ;
}

As we can see the if statement is gone, and the main function is simpler. Using subroutines allows for cleaner code. On the other hand it may be harder to debug since there’s one more item to keep track of.

We can have multiple subroutine types, and each can have many functions defined. Actually, as long as the signatures are compatible we can write a function that can be used with different types.

subroutine vec4 colorRedBlue();
subroutine vec4 colorRed();

subroutine (colorRedBlue, colorRed ) vec4 redColor() {

	return vec4(1.0, 0.0, 0.0, 1.0);
}

On the application side we have to select which subroutine is going to be used. Each routine is assigned an index, and retrieving these indices can be achieved with the following function:

[stextbox]GLuint glGetSubroutineIndex(GLuint program, GLenum shaderType, const GLchar *name);

Parameters:

  • program: the name of the program
  • shaderType:  the shader stage where the routine is defined. Must be one of GL_VERTEX_SHADERGL_TESS_CONTROL_SHADERGL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, or GL_FRAGMENT_SHADER
  • name: the name of the subroutine

[/stextbox]

We also need to query the subroutine uniform location using function

[stextbox]GLint glGetSubroutineUniformLocation(GLuint program, GLenum shaderType, const GLchar *name);

Parameters:

  • program: the name of the program
  • shaderType:  the shader stage where the routine is defined. Must be one of GL_VERTEX_SHADERGL_TESS_CONTROL_SHADERGL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, or GL_FRAGMENT_SHADER
  • name: the name of the subroutine uniform

[/stextbox]

Getting back to our example, we would write:

GLuint routineC1 = glGetSubroutineIndex(p, GL_VERTEX_SHADER, "redColor");
GLuint routineC2 = glGetSubroutineIndex(p, GL_VERTEX_SHADER, "blueColor");
GLuint v1 = glGetSubroutineUniformLocation(shader.getProgramIndex(), GL_VERTEX_SHADER, "myRedBlueSelection");

Finally we need to establish which subroutine is assigned to each subroutine type. Since we can have multiple types, we need an array where the index of the array is the subroutine uniform location, and the value is the subroutine. The function that establishes this connection is:
[stextbox]GLint glUniformSubroutinesuiv(GLenum shaderType, , GLsizei count, const GLuint *indices);

Parameters:

  • shaderType:  the shader stage where the routine is defined. Must be one of GL_VERTEX_SHADERGL_TESS_CONTROL_SHADERGL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, or GL_FRAGMENT_SHADER
  • count: the number of elements in the array
  • name: the array with the associations between the subroutine uniform locations and the subroutine indexes

[/stextbox]

Note that a program must be in use before the above function is called. Furthermore, this association is not part of the program’s state, and it gets lost as soon as the program is re-linked or glUseProgram, glBindProgramPipeline, or glUseProgramStages are called.

In practice, this implies that we should call glUniformSubroutinesuiv somewhere between calling glUseProgram and the drawing command.

The following OpenGL code snippet shows how to retrieve information about the subroutine uniforms and its compatible subroutines. It presents some of the functions that OpenGL provides to query information about subroutines.

int maxSub,maxSubU,activeS,countActiveSU;
char name[256]; int len, numCompS;

glGetIntegerv(GL_MAX_SUBROUTINES, &maxSub);
glGetIntegerv(GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, &maxSubU);
printf("Max Subroutines: %d  Max Subroutine Uniforms: %d\n", maxSub,maxSubU);

glGetProgramStageiv(p, GL_VERTEX_SHADER, GL_ACTIVE_SUBROUTINE_UNIFORMS, &countActiveSU);

for (int i = 0; i < countActiveSU; ++i) {
	
	glGetActiveSubroutineUniformName(p, GL_VERTEX_SHADER, i, 256, &len, name);

	printf("Suroutine Uniform: %d name: %s\n", i,name);
	glGetActiveSubroutineUniformiv(p, GL_VERTEX_SHADER, i, GL_NUM_COMPATIBLE_SUBROUTINES, &numCompS);
	
	int *s = (int *)malloc(sizeof(int) * numCompS);
	glGetActiveSubroutineUniformiv(p, GL_VERTEX_SHADER, i, GL_COMPATIBLE_SUBROUTINES, s);
	printf("Compatible Subroutines:\n");
	for (int j=0; j < numCompS; ++j) {
	
		glGetActiveSubroutineName(p, GL_VERTEX_SHADER, s[j], 256, &len, name);
		printf("\t%d - %s\n", s[j],name);
	}
	printf("\n");
	free(s);
}

The above code produces results such as the following:

Max Subroutines: 1024  Max Subroutine Uniforms: 1024
Suroutine Uniform: 0 name: myGreenSelection
Compatible Subroutines:
        2 - doNotUseGreen
        3 - useGreen

Suroutine Uniform: 1 name: myIntensitySelection
Compatible Subroutines:
        4 - halfIntensity
        5 - fullIntensity

Suroutine Uniform: 2 name: myRedBlueSelection
Compatible Subroutines:
        0 - blueColor
        1 - redColor

 

Prev: Statements and Functions Next: Comm. App=>Shader
 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: