- Shaders and OpenGL
- OpenGL's Programmable Pipeline
- An Overview of the OpenGL Shading Language
- Interface Blocks
- Compiling Shaders
- Shader Subroutines
- Separate Shader Objects
- SPIR-V
An Overview of the OpenGL Shading Language
This section provides an overview of the shading language used within OpenGL. GLSL shares many traits with C++ and Java, and is used for authoring shaders for all the stages supported in OpenGL, although certain features are available only for particular types of shaders. We will first describe GLSL’s requirements, types, and other language constructs that are shared between the various shader stages, and then discuss the features unique to each type of shader.
Creating Shaders with GLSL
Here, we describe how to create a complete shader.
The Starting Point
A shader program, just like a C program, starts execution in main(). Every GLSL shader program begins life as follows:
#version 330 core void main() { // Your code goes here }
The // construct is a comment and terminates at the end of the current line, just like in C. Additionally, C-type, multi-line comments—the /* and */ type—are also supported. However, unlike ANSI C, main() does not return an integer value; it is declared void. Also, as with C and its derivative languages, statements are terminated with a semicolon. While this is a perfectly legal GLSL program that compiles and even runs, its functionality leaves something to be desired. To add a little more excitement to our shaders, we’ll continue by describing variables and their operation.
Declaring Variables
GLSL is a typed language; every variable must be declared and have an associated type. Variable names conform to the same rules as those for C: You can use letters, numbers, and the underscore character (_) to compose variable names. However, a digit cannot be the first character in a variable name. Similarly, variable names cannot contain consecutive underscores; those names are reserved in GLSL.
Table 2.1 shows the basic types available in GLSL.
Table 2.1 Basic Data Types in GLSL
Type |
Description |
---|---|
float |
IEEE 32-bit floating-point value |
double |
IEEE 64-bit floating-point value |
int |
signed two´s-complement 32-bit integer value |
uint |
unsigned 32-bit integer value |
bool |
Boolean value |
These types (and later, aggregate types composed of these) are all transparent. That is, their internal form is exposed and the shader code gets to assume what they look like internally.
An additional set of types, the opaque types, do not have their internal form exposed. These include sampler types, image types, and atomic counter types. They declare variables used as opaque handles for accessing texture maps, images, and atomic counters, as described in Chapter 4, “Color, Pixels, and Fragments.”
The various types of samplers and their uses are discussed in Chapter 6, “Textures and Framebuffers.”
Variable Scoping
While all variables must be declared, they may be declared any time before their use (just as in C++). The scoping rules of GLSL, which closely parallel those of C++, are as follows:
Variables declared outside of any function definition have global scope and are visible to all subsequent functions within the shader program.
Variables declared within a set of curly braces (e.g., function definition, block following a loop or “if” statement, etc.) exist within the scope of those braces only.
Loop iteration variables, such as i in the loop
for (int i = 0; i < 10; ++i) { // loop body }
are scoped only for the body of the loop.
Variable Initialization
Variables may also be initialized when declared. For example:
int i, numParticles = 1500; float force, g = -9.8; bool falling = true; double pi = 3.1415926535897932384626LF;
Integer literal constants may be expressed as octal, decimal, or hexadecimal values. An optional minus sign before a numeric value negates the constant, and a trailing ‘u’ or ‘U’ denotes an unsigned integer value.
Floating-point literals must include a decimal point, unless described in scientific format, as in 3E-7. (However, there are many situations where an integer literal will be implicitly converted to a floating-point value.) Additionally, they may optionally include an ‘f’ or ‘F’ suffix as in C on a float literal. You must include a suffix of ‘lF’ or ‘LF’ to make a literal have the precision of a double.
Boolean values are either true or false and can be initialized to either of those values or as the result of an operation that resolves to a Boolean expression.
Constructors
As mentioned, GLSL is more type safe than C++, having fewer implicit conversion between values. For example,
int f = false;
will result in a compilation error due to assigning a Boolean value to an integer variable. Types will be implicitly converted as shown in Table 2.2.
Table 2.2 Implicit Conversions in GLSL
Type Needed |
Can Be Implicitly Converted From |
---|---|
uint |
int |
float |
int, uint |
double |
int, uint, float |
These type conversions work for scalars, vectors, and matrices of these types. Conversions will never change whether something is a vector or a matrix, or how many components it has. Conversions also don’t apply to arrays or structures.
Any other conversion of values requires explicit conversion using a conversion constructor. A constructor, as in other languages like C++, is a function with the same name as a type, which returns a value of that type. For example,
float f = 10.0; int ten = int(f);
uses an int conversion constructor to do the conversion. Likewise, the other types also have conversion constructors: float, double, uint, bool, and vectors and matrices of these types. Each accepts multiple other types to explicitly convert from. These functions also illustrate another feature of GLSL: function overloading, whereby each function takes various input types, but all use the same base function name. We will discuss more on functions in a bit.
Aggregate Types
GLSL’s basic types can be combined to better match core OpenGL’s data values and to ease computational operations.
First, GLSL supports vectors of two, three, or four components for each of the basic types of bool, int, uint, float, and double. Also, matrices of float and double are available. Table 2.3 lists the valid vector and matrix types.
Table 2.3 GLSL Vector and Matrix Types
Base Type |
2D Vec |
3D Vec |
4D Vec |
Matrix Types |
---|---|---|---|---|
float |
vec2 |
vec3 |
vec4 |
mat2 mat3 mat4 |
double |
dvec2 |
dvec3 |
dvec4 |
dmat2 dmat3 dmat4 |
int |
ivec2 |
ivec3 |
ivec4 |
‐ |
uint |
uvec2 |
uvec3 |
uvec4 |
‐ |
bool |
bvec2 |
bvec3 |
bvec4 |
‐ |
Matrix types that list both dimensions, such as mat4x3, use the first value to specify the number of columns, the second the number of rows.
Variables declared with these types can be initialized similar to their scalar counterparts:
vec3 velocity = vec3(0.0, 2.0, 3.0);
Converting between types is equally accessible:
ivec3 steps = ivec3(velocity);
Vector constructors can also be used to truncate or lengthen a vector. If a longer vector is passed into the constructor of a smaller vector, the vector is truncated to the appropriate length.
vec4 color; vec3 RGB = vec3(color); // now RGB only has three elements
Scalar values can be promoted to vectors, but that’s the only way a vector constructor takes fewer components than its size indicates:
vec3 white = vec3(1.0); // white = (1.0, 1.0, 1.0) vec4 translucent = vec4(white, 0.5);
Matrices are constructed in the same manner and can be initialized to either a diagonal matrix or a fully populated matrix. In the case of diagonal matrices, a single value is passed into the constructor, and the diagonal elements of the matrix are set to that value, with all others being set to zero, as in
Matrices can also be created by specifying the value of every element in the matrix in the constructor. Values can be specified by combinations of scalars and vectors as long as enough values are provided and each column is specified in the same manner. Additionally, matrices are specified in column-major order, meaning the values are used to populate columns before rows (which is the opposite of how C initializes two-dimensional arrays).
For example, we could initialize a 3 × 3 matrix in any of the following ways:
mat3 M = mat3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); vec3 column1 = vec3(1.0, 2.0, 3.0); vec3 column2 = vec3(4.0, 5.0, 6.0); vec3 column3 = vec3(7.0, 8.0, 9.0); mat3 M = mat3(column1, column2, column3);
or even
vec2 column1 = vec2(1.0, 2.0); vec2 column2 = vec2(4.0, 5.0); vec2 column3 = vec2(7.0, 8.0); mat3 M = mat3(column1, 3.0, column2, 6.0, column3, 9.0);
all yielding the same matrix,
Accessing Elements in Vectors and Matrices
The individual elements of vectors and matrices can be accessed and assigned. Vectors support two types of element access: a named-component method and an array-like method. Matrices use a two-dimensional array-like method.
Components of a vector can be accessed by name, as in
float red = color.r; float v_y = velocity.y;
or by using a zero-based index scheme. The following yield identical results to the previous listing:
float red = color[0]; float v_y = velocity[1];
In fact, as shown in Table 2.4, there are three sets of component names, all of which do the same thing. The multiple sets are useful for clarifying the operations that you’re doing.
Table 2.4 Vector Component Accessors
Component Accessors |
Description |
---|---|
(x, y, z,w) |
Components associated with positions |
(r, g, b, a) |
Components associated with colors |
(s, t, p, q) |
Components associated with texture coordinates |
A common use for component-wise access to vectors is for swizzling components, as you might do with colors, perhaps for color space conversion. For example, you could do the following to specify a luminance value based on the red component of an input color:
vec3 luminance = color.rrr;
Likewise, if you needed to move components around in a vector, you might do
color = color.abgr; // reverse the components of a color
The only restriction is that only one set of components can be used with a variable in one statement. That is, you can’t do
vec4 color = otherColor.rgz; // Error: 'z' is from a different group
Also, a compile-time error will be raised if you attempt to access an element that’s outside the range of the type. For example,
vec2 pos; float zPos = pos.z; // Error: no 'z' component in 2D vectors
Matrix elements can be accessed using the array notation. Either a single scalar value or an array of elements can be accessed from a matrix:
mat4 m = mat4(2.0); vec4 zVec = m[2]; // get column 2 of the matrix float yScale = m[1][1]; // or m[1].y works as well
Structures
You can also logically group collections of different types into a structure. Structures are convenient for passing groups of associated data into functions. When a structure is defined, it automatically creates a new type and implicitly defines a constructor function that takes the types of the elements of the structure as parameters.
struct Particle { float lifetime; vec3 position; vec3 velocity; }; Particle p = Particle(10.0, pos, vel); // pos, vel are vec3s
Likewise, to reference elements of a structure, use the familiar “dot” notation as you would in C.
Arrays
GLSL also supports arrays of any type, including structures. As with C, arrays are indexed using brackets ([ ]). The range of elements in an array of size n is 0 ... n – 1. Unlike in C, however, neither negative array indices nor positive indices out of range are permitted. As of GLSL 4.3, arrays can be made out of arrays, providing a way to handle multidimensional data. However, GLSL 4.2 and earlier versions do not allow arrays of arrays to be created (that is, you cannot create a multidimensional array).
Arrays can be declared sized or unsized. You might use an unsized array as a forward declaration of an array variable and later redeclare it to the appropriate size. Array declarations use the bracket notation, as in
float coeff[3]; // an array of 3 floats float[3] coeff; // same thing int indices[]; // unsized. Redeclare later with a size
Arrays are first-class types in GLSL, meaning they have constructors and can be used as function parameters and return types. To statically initialize an array of values, you would use a constructor in the following manner:
float coeff[3] = float[3](2.38, 3.14, 42.0);
The dimension value on the constructor is optional.
Additionally, similar to Java, GLSL arrays have an implicit method for reporting their number of elements: the length() method. If you would like to operate on all the values in an array, here is an example using the length() method:
for (int i = 0; i < coeff.length(); ++i) { coeff[i] *= 2.0; }
The length() method also works on vectors and matrices. A vector’s length is the number of components it contains, while a matrix’s length is the number of columns it contains. This is exactly what you need when using array syntax for indexing vectors and matrices. (m[2] is the third column of a matrix m.)
mat3x4 m; int c = m.length(); // number of columns in m: 3 int r = m[0].length(); // number of components in column vector 0: 4
When the length is known at compile time, the length() method will return a compile-time constant that can be used where compile-time constants are required. For example:
mat4 m; float diagonal[m.length()]; // array of size matching the matrix size float x[gl_in.length()]; // array of size matching the number of // geometry shader input vertices
For all vectors and matrices, and most arrays, length() is known at compile time. However, for some arrays, length() is not known until link time. This happens when relying on the linker to deduce the size from multiple shaders in the same stage. For shader storage buffer objects (declared with buffer, as described shortly), length() might not be known until render time. If you want a compile-time constant returned from length(), just make sure you establish the array size in your shader before using the length() method.
Multidimensional arrays are really arrays made from arrays and have a syntax similar to C:
float coeff[3][5]; // an array of size 3 of arrays of size 5 coeff[2][1] *= 2.0; // inner-dimension index is 1, outer is 2 coeff.length(); // this returns the constant 3 coeff[2]; // a one-dimensional array of size 5 coeff[2].length(); // this returns the constant 5
Multidimensional arrays can be formed in this way for virtually any type and resource. When shared with the application, the innermost (rightmost) dimension changes the fastest in the memory layout.
Storage Qualifiers
Types can also have modifiers that affect their behavior. There are several modifiers defined in GLSL, as shown in Table 2.5, with the behaviors they exhibit when used at global scope.
Table 2.5 GLSL Type Modifiers
Type Modifier |
Description |
---|---|
const |
Labels a variable as read-only. It will also be a compile-time constant if its initializer is a compile-time constant. |
in |
Specifies that the variable is an input to the shader stage. |
out |
Specifies that the variable is an output from a shader stage. |
uniform |
Specifies that the value is passed to the shader from the application and is constant across a given primitive. |
buffer |
Specifies read-write memory shared with the application. This memory is also referred to as a shader storage buffer. |
shared |
Specifies that the variables are shared within a local work group. This is used only in compute shaders. |
const Storage Qualifier
Just as with C, const type modifier indicates that the variable is read-only. For example, the statement
const float Pi = 3.141529;
sets the variable Pi to an approximation of π. With the addition of the const modifier, it becomes an error to write to a variable after its declaration, so const variables must be initialized when declared.
in Storage Qualifier
The in modifier is used to qualify inputs into a shader stage. Those inputs may be vertex attributes (for vertex shaders) or output variables from the preceding shader stage.
Fragment shaders can further qualify their input values using some additional keywords that we discuss in Chapter 4, “Color, Pixels, and Fragments.”
out Storage Qualifier
The out modifier is used to qualify outputs from a shader stage. For example, the transformed homogeneous coordinates from a vertex shader or the final fragment color from a fragment shader.
uniform Storage Qualifier
The uniform modifier specifies that a variable’s value will be specified by the application before the shader’s execution and does not change across the primitive being processed. Uniform variables are shared among all the shader stages enabled in a program and must be declared as global variables. Any type of variable, including structures and arrays, can be specified as uniform. A shader cannot write to a uniform variable and change its value.
For example, you might want to use a color for shading a primitive. You might declare a uniform variable to pass that information into your shaders. In the shaders, you would make the declaration
uniform vec4 BaseColor;
Within your shaders, you can reference BaseColor by name, but to set its value in your application, you need to do a little extra work. The GLSL compiler creates a table of all uniform variables when it links your shader program. To set BaseColor’s value from your application, you need to obtain the index of BaseColor in the table, which is done using the glGetUniformLocation() routine.
Once you have the associated index for the uniform variable, you can set the value of the uniform variable using the glUniform*() or glUniformMatrix*() routine. Example 2.2 demonstrates obtaining a uniform variable’s index and assigning values.
Example 2.2 Obtaining a Uniform Variable’s Index and Assigning Values
GLint timeLoc; /* Uniform index for variable "time" in shader */ GLfloat timeValue; /* Application time */ timeLoc = glGetUniformLocation(program, "time"); glUniform1f(timeLoc, timeValue);
buffer Storage Qualifier
The recommended way to share a large buffer with the application is through use of a buffer variable. Buffer variables are much like uniform variables, except that they can be modified by the shader. Typically, you’d use buffer variables in a buffer block, and blocks in general are described later in this chapter.
The buffer modifier specifies that the subsequent block is a memory buffer shared between the shader and the application. This buffer is both readable and writable by the shader. The size of the buffer can be established after shader compilation and program linking.
shared Storage Qualifier
The shared modifier is used only in compute shaders to establish memory shared within a local work group. This is discussed in more detail in Chapter 12, “Compute Shaders.”
Statements
The real work in a shader is done by computing values and making decisions. In the same manner as C++, GLSL has a rich set of operators for constructing arithmetic operations for computing values and a standard set of logical constructs for controlling shader execution.
Arithmetic Operations
No text describing a language is complete without the mandatory table of operator precedence (see Table 2.6). The operators are ordered in decreasing precedence. In general, the types being operated on must be the same, and for vector and matrices, the operands must be of the same dimension. In the table, integer types include int and uint and vectors of them; floating-point types include float and double types and vectors and matrices of them; arithmetic types include all integer and floating-point types; and any additionally includes structures and arrays.
Table 2.6 GLSL Operators and Their Precedence
Precedence |
Operators |
Accepted Types |
Description |
---|---|---|---|
1 |
( ) |
‐ |
Grouping of operations |
2 |
[ ] |
arrays, matrices, vectors |
Array subscripting |
3 |
++ -- |
arithmetic |
Pre-increment and -decrement |
4 |
* / % |
arithmetic |
Multiplicative operations |
5 |
+ - |
arithmetic |
Additive operations |
6 |
<< >> |
integer |
Bit-wise operations |
7 |
< > <= >= |
arithmetic |
Relational operations |
8 |
== != |
any |
Equality operations |
9 |
& |
integer |
Bit-wise and |
10 |
∧ |
integer |
Bit-wise exclusive or |
11 |
| |
integer |
Bit-wise inclusive or |
12 |
&& |
bool |
Logical and operation |
13 |
∧∧ |
bool |
Logical exclusive-or operation |
14 |
|| |
bool |
Logical or operation |
15 |
a ? b : c |
bool ? any : any |
Ternary selection operation (inline “if„ operation; if (a) then (b) else (c)) |
16 |
= |
any |
Assignment |
17 |
,(comma) |
any |
Sequence of operations |
Overloaded Operators
Most operators in GLSL are overloaded, meaning that they operate on a varied set of types. Specifically, arithmetic operations (including pre- and post-increment and -decrement) for vectors and matrices are well defined in GLSL. For example, to multiply a vector and a matrix (recalling that the order of operands is important; matrix multiplication is noncommutative, for all you math heads), use the following operation:
vec3 v; mat3 m; vec3 result = v * m;
The normal restrictions apply, that the dimensionality of the matrix and the vector must match. Additionally, scalar multiplication with a vector or matrix will produce the expected result. One notable exception is that the multiplication of two vectors will result in component-wise multiplication of components; however, multiplying two matrices will result in normal matrix multiplication.
vec2 a, b, c; mat2 m, u, v; c = a * b; // c = (a.x*b.x, a.y*b.y) m = u * v; // m = (u00*v00+u01*v10 u00*v01+u01*v11 // u01*v00+u11*v10 u10*v01+u11*v11)
Additional common vector operations (e.g., dot and cross products) are supported by function calls, as well as various per-component operations on vectors and matrices.
Control Flow
GLSL’s logical control structures are the popular if-else and switch statements. As with the C language, the else clause is optional, and multiple statements require a block.
if (truth) { // true clause } else { // false clause }
Similar to the situation in C, switch statements are available (starting with GLSL 1.30) in their familiar form:
switch (int_value) { case n: // statements break; case m: // statements break; default: // statements break; }
GLSL switch statements also support “fall-through” cases—case statements that do not end with break statements. Each case does require some statement to execute before the end of the switch (before the closing brace). Also, unlike in C++, no statements are allowed before the first case. If no case matches the switch and a default label is present, then it is executed.
Looping Constructs
GLSL supports the familiar C form of for, while, and do ... while loops.
The for loop permits the declaration of the loop iteration variable in the initialization clause of the for loop. The scope of iteration variables declared in this manner is only for the lifetime of the loop.
for (int i = 0; i < 10; ++i) { ... } while (n < 10) { ... } do { ... } while (n < 10);
Control-Flow Statements
Additional control statements beyond conditionals and loops are available in GLSL. Table 2.7 describes available control-flow statements.
Table 2.7 GLSL Control-Flow Statements
Statement |
Description |
---|---|
break |
Terminates execution of the block of a loop and continues execution after the scope of that block. |
continue |
Terminates the current iteration of the enclosing block of a loop, resuming execution with the next iteration of the loop. |
return [result] |
Returns from the current function, optionally providing a value to be returned from the function (assuming return value matches the return type of the enclosing function). |
discard |
Discards the current fragment and ceases shader execution. Discard statements are valid only in fragment shader programs. |
The discard statement is available only in fragment programs. The execution of the fragment shader may be terminated at the execution of the discard statement, but this is implementation-dependent.
Functions
Functions permit you to replace occurrences of common code with a function call. This, of course, allows for smaller code and fewer chances for errors. GLSL defines a number of built-in functions, which are listed in Appendix C, as well as support for user-defined functions. User-defined functions can be defined in a single shader object and reused in multiple shader programs.
Declarations
Function declaration syntax is very similar to C, with the exception of the access modifiers on variables:
returnType functionName([accessModifier] type1 variable1, [accessModifier] type2 variable2, ...) { // function body return returnValue; // unless returnType is void }
Function names can be any combination of letters, numbers, and the underscore character, with the exception that it can neither begin with a digit nor with gl_ nor contain consecutive underscores.
Return types can be any built-in GLSL type or user-defined structure or array type. Arrays as return values must explicitly specify their size. If a function doesn’t return a value, its return type is void.
Parameters to functions can also be of any type, including arrays (which must specify their size).
Functions must be either declared with a prototype or defined with a body before they are called. Just as in C++, the compiler must have seen the function’s declaration before its use, or an error will be raised. If a function is used in a shader object other than the one where it’s defined, a prototype must be declared. A prototype is merely the function’s signature without its accompanying body. Here’s a simple example:
float HornerEvalPolynomial(float coeff[10], float x);
Parameter Qualifiers
While functions in GLSL are able to modify and return values after their execution, there’s no concept of a pointer or reference, as in C or C++. Rather, parameters of functions have associated parameter qualifiers indicating whether the value should be copied into, or out of, a function after execution. Table 2.8 describes the available parameter qualifiers in GLSL.
Table 2.8 GLSL Function Parameter Access Modifiers
Access Modifier |
Description |
---|---|
in |
Value copied into a function (default if not specified) |
const in |
Read-only value copied into a function |
out |
Value copied out of a function (undefined upon entrance into the function) |
inout |
Value copied into and out of a function |
The in keyword is optional. If a variable does not include an access modifier, an in modifier is implicitly added to the parameter’s declaration. However, if the variable’s value needs to be copied out of a function, it must either be tagged with an out (for copy out-only variables) or an inout (for a variable both copied in and copied out) modifier. Writing to a variable not tagged with one of these modifiers will generate a compile-time error.
Additionally, to verify at compile time that a function doesn’t modify an input-only variable, adding a const in modifier will cause the compiler to check that the variable is not written to in the function. If you don’t do this and do write to an input-only variable, the write only modifies the local copy in the function.
Computational Invariance
GLSL does not guarantee that two identical computations in different shaders will result in exactly the same value. The situation is no different than for computational applications executing on the CPU, where the choice of optimizations may result in tiny differences in results. These tiny errors may be an issue for multipass algorithms that expect positions to be computed exactly the same for each shader pass. GLSL has two methods for enforcing this type of invariance between shaders, using the invariant or precise keywords.
Both of these methods will cause computations done by the graphics device to create reproducibility (invariance) in results of the same expression. However, they do not help reproduce the same results between the host and the graphics device. Compile-time constant expressions are computed on the compiler’s host, and there is no guarantee that the host computes in exactly the same way as the graphics device. For example:
uniform float ten; // application sets this to 10.0 const float f = sin(10.0); // computed on compiler host float g = sin(ten); // computed on graphics device void main() { if (f == g) // f and g might be not equal ; }
In this example, it would not matter if invariant or precise was used on any of the variables involved, as they affect only two computations done on the graphics device.
The invariant Qualifier
The invariant qualifier may be applied to any shader output variable. It will guarantee that if two shader invocations each set the output variable with the same expression and the same values for the variables in that expression, both will compute the same value.
The output variable declared as invariant may be a built-in variable or a user-defined one. For example:
invariant gl_Position; invariant centroid out vec3 Color;
As you may recall, output variables are used to pass data from one stage to the next. The invariant keyword may be applied at any time before use of the variable in the shader and may be used to modify built-in variables. This is done by declaring the variable only with invariant, as was shown earlier for gl_Position.
For debugging, it may be useful to impose invariance on all varying variables in shader. This can be accomplished by using the vertex shader preprocessor pragma.
#pragma STDGL invariant(all)
Global invariance in this manner is useful for debugging; however, it may likely have an impact on the shader’s performance. Guaranteeing invariance usually disables optimizations that may have been performed by the GLSL compiler.
The precise Qualifier
The precise qualifier may be applied to any computed variable or function return value. Despite its name, its purpose is not to increase precision, but to increase reproducibility of a computation. It is mostly used in tessellation shaders to avoid forming cracks in your geometry. Tessellation shading in general is described in Chapter 9, “Tessellation Shaders,” and there is additional discussion in that chapter about a use case for precise qualification.
Generally, you use precise instead of invariant when you need to get the same result from an expression, even if values feeding the expression are permuted in a way that should not mathematically affect the result. For example, the following expression should get the same result if the values for a and b are exchanged. It should also get the same result if the values for c and d and exchanged, or if both a and c are exchanged and b and d are exchanged, and so on.
Location = a * b + c * d;
The precise qualifier may be applied to a built-in variable, user variable, or function return value.
precise gl_Position; precise out vec3 Location; precise vec3 subdivide(vec3 P1, vec3 P2) { ... }
The precise keyword may be applied at any time before use of the variable in the shader and may be used to modify previously declared variables.
One practical impact in a compiler of using precise is an expression like the one above cannot be evaluated using two different methods of multiplication for the two multiply operations—for example, a multiply instruction for the first multiply and a fused multiply-and-add instruction for the second multiply. This is because these two instructions will get slightly different results for the same values. Because that was disallowed by precise, the compiler is prevented from doing this. Because use of fused multiply-and-add instructions is important to performance, it would be unfortunate to completely disallow them. So there is a built-in function in GLSL, fma(), that you can use to explicitly say this is okay.
precise out float result; ... float f = c * d; float result = fma(a, b, f);
Of course, you do that only if you weren’t going to have the values of a and c permuted, as you would be defeating the purpose of using precise.
Shader Preprocessor
The first step in compilation of a GLSL shader is parsing by the preprocessor. Similar to the C preprocessor, there are a number of directives for creating conditional compilation blocks and defining values. However, unlike in the C preprocessor, there is no file inclusion (#include).
Preprocessor Directives
Table 2.9 lists the preprocessor directives accepted by the GLSL preprocessor and their functions.
Table 2.9 GLSL Preprocessor Directives
Preprocessor Directive |
Description |
---|---|
#define |
Control the definition of constants and |
#undef |
macros similar to the C preprocessor. |
#if |
Conditional code management similar |
#ifdef |
to the C preprocessor, including the defined operator. |
#ifndef |
|
#else |
Conditional expressions evaluate integer |
#elif |
expressions and defined values |
#endif |
(as specified by #define) only. |
#error text |
Cause the compiler to insert text (up to the first newline character) into the shader information log. |
#pragma options |
Control compiler specific options. |
#extension options |
Specify compiler operation with respect to specified GLSL extensions. |
#version number |
Mandate a specific version of GLSL version support. |
#line options |
Control diagnostic line numbering. |
Macro Definition
The GLSL preprocessor allows macro definition in much the same manner as the C preprocessor, with the exception of the string substitution and concatenation facilities. Macros might define a single value, as in
#define NUM_ELEMENTS 10
or with parameters like
#define LPos(n) gl_LightSource[(n)].position
Additionally, there are several predefined macros for aiding in diagnostic messages (that you might issue with the #error directive, for example), as shown in Table 2.10.
Table 2.10 GLSL Preprocessor Predefined Macros
Macro Name |
Description |
---|---|
_LINE_ |
Line number defined by one more than the number of newline characters processed and modified by the #line directive |
_FILE_ |
Source string number currently being processed |
_VERSION_ |
Integer representation of the OpenGL Shading Language version |
Likewise, macros (excluding those defined by GLSL) may be undefined by using the #undef directive. For example,
#undef LPos
Preprocessor Conditionals
Identical to the processing by the C preprocessor, the GLSL preprocessor provides conditional code inclusion based on macro definition and integer constant evaluation.
Macro definition may be determined in two ways. Use the #ifdef directive:
#ifdef NUM_ELEMENTS ... #endif
Or use the defined operator with the #if or #elif directives:
#if defined(NUM_ELEMENTS) && NUM_ELEMENTS > 3 ... #elif NUM_ELEMENTS < 7 ... #endif
Compiler Control
The #pragma directive provides the compiler additional information regarding how you would like your shaders compiled.
Optimization Compiler Option
The optimize option instructs the compiler to enable or disable optimization of the shader from the point where the directive resides forward in the shader source. You can enable or disable optimization by issuing either
#pragma optimize(on)
or
#pragma optimize(off)
respectively. These options may be issued only outside of a function definition. By default, optimization is enabled for all shaders.
Debug Compiler Option
The debug option enables or disables additional diagnostic output of the shader. You can enable or disable debugging by issuing either
#pragma debug(on)
or
#pragma debug(off)
respectively. As with the optimize option, these options may be issued only outside of a function definition, and by default, debugging is disabled for all shaders.
Global Shader-Compilation Option
One final #pragma directive that is available is STDGL. This option is currently used to enable invariance in the output of varying values.
Extension Processing in Shaders
GLSL, like OpenGL itself, may be enhanced by extensions. As vendors may include extensions specific to their OpenGL implementation, it’s useful to have some control over shader compilation in light of possible extensions that a shader may use.
The GLSL preprocessor uses the #extension directive to provide instructions to the shader compiler regarding how extension availability should be handled during compilation. For any or all extensions, you can specify how you would like the compiler to proceed with compilation:
#extension extension_name : <directive>
for a single extension, or
#extension all : <directive>
which affects the behavior of all extensions.
The options available are shown in Table 2.11
Table 2.11 GLSL Extension Directive Modifiers
Directive |
Description |
---|---|
require |
Flag an error if the extension is not supported or if the all-extension specification is used. |
enable |
Give a warning if the particular extensions specified are not supported, or flag an error if the all-extension specification is used. |
warn |
Give a warning if the particular extensions specified are not supported, or give a warning if any extension use is detected during compilation. |
disable |
Disable support for the particular extensions listed (that is, have the compiler act as if the extension is not supported even if it is) or all extensions if all is present, issuing warnings and errors as if the extension were not present. |