Chapter 5. OpenGL ES Shading Language

As you have seen from Chapter 1, “Introduction to OpenGL ES 2.0,” Chapter 2, “Hello, Triangle: An OpenGL ES 2.0 Example,” and Chapter 4, “Shaders and Programs,” shaders are a fundamental concept that is at the heart of the OpenGL ES 2.0 API. Every OpenGL ES 2.0 program requires both a vertex and fragment shader to render a meaningful picture. Given the centrality of the concept of shaders to the API, we want to make sure you are grounded in the fundamentals of writing shaders before diving into more details of the graphics API.

Our goal in this chapter is to make sure you understand the following concepts in the shading language:

  • Variables and variable types

  • Vector and matrix construction and selection

  • Constants

  • Structures and arrays

  • Operators, control flow, and functions

  • Attributes, uniforms, and varyings

  • Preprocessor and directives

  • Uniform and varying packing

  • Precision qualifiers and invariance

You were introduced to some of these concepts in a small amount of detail with the Hello Triangle example in Chapter 2. Now we are going to fill in the concepts with a lot more detail to make sure you understand how to write and read shaders.

OpenGL ES Shading Language Basics

As you read through this book, you are going to be looking at a lot of shaders. If you ever start developing your own OpenGL ES 2.0 application, chances are you will be writing a lot of shaders. By now, you should understand the fundamental concepts of what a shader does and how it fits in the pipeline. If not, please go back and review Chapter 1, where we covered the pipeline and described where vertex and fragment shaders fit in.

What we want to look at now is what exactly makes up a shader. As you have probably already observed, the syntax bears great similarity to C. If you can understand C code, you likely will not have much difficulty understanding the syntax of shaders. However, there are certainly some major differences between the two languages, beginning with the native data types that are supported.

Variables and Variable Types

In computer graphics, there are two fundamental data types that form the basis of transformations: vectors and matrices. These two data types are central to the OpenGL ES Shading Language as well. Specifically, Table 5-1 describes the scalar-, vector-, and matrix-based data types that exist in the shading language.

Table 5-1. Data Types in the OpenGL ES Shading Language

Variable Class

Types

Description

Scalars

float, int, bool

Scalar-based data types for float-point, integer, and boolean values

Floating-point vectors

float, vec2, vec3, vec4

Floating-point-based vector types of one, two, three, or four components

Integer vector

int, ivec2, ivec3, ivec4

Integer-based vector types of one, two, three, or four components

Boolean vector

bool, bvec2, bvec3, bvec4

Boolean-based vector types of one, two, three, or four components

Matrices

mat2, mat3, mat4

Floating-point based matrices of size 2 × 2, 3 × 3, or 4 × 4

Variables in the shading language must be declared with a type. For example, the following declarations are examples of how to declare a scalar, vector, or matrix:

float specularAtten;   // A floating-point-based scalar
vec4  vPosition;       // A floating-point-based 4-tuple vector
mat4  mViewProjection; // A 4 x 4 matrix variable declaration
ivec2 vOffset;         // An integer-based 2-tuple vector

Variables can be initialized either at declaration time or later. Initialization is done through the use of constructors, which are also used for doing type conversions.

Variable Constructors

The OpenGL ES Shading Language has very strict rules regarding type conversion. That is, variables can only be assigned to or operated on other variables of the same type. To cope with type conversions, there are a number of constructors available in the language. You can use constructors for initializing variables and as a way of type-casting between variables of different types. Variables can be initialized at declaration (or later in the shader) through the use of constructors. Each of the built-in variable types has a set of associated constructors.

Let’s first take a look at how constructors can be used to initialize and type cast between scalar values.

float myFloat = 1.0;
bool  myBool = true;
int   myInt = 0;
myFloat = float(myBool); // Convert from bool -> float
myFloat = float(myInt);  // Convert from int -> float
myBool  = bool(myInt);   // Convert from int -> bool

Similarly, constructors can be used to convert to and initialize vector data types. The arguments to a vector constructor will be converted to the same basic type as the vector being constructed (float, int, or bool). There are two basic ways to pass arguments to vector constructors:

  • If only one scalar argument is provided to a vector constructor, that value is used to set all values of the vector.

  • If multiple scalar or vector arguments are provided, the values of the vector are set from left to right using those arguments. If multiple scalar arguments are provided, there must be at least as many components in the arguments as in the vector.

The following shows some examples of constructing vectors:

vec4 myVec4 = vec4(1.0);           // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5}
vec3 temp   = vec3(myVec3);        // temp = myVec3
vec2 myVec2 = vec2(myVec3);        // myVec2 = {myVec3.x, myVec3.y}

myVec4 = vec4(myVec2, temp, 0.0);  // myVec4 = {myVec2.x, myVec2.y,
                                   //           temp, 0.0 }

For matrix construction, the language is very flexible. These basic rules describe how matrices can be constructed:

  • If only one scalar argument is provided to a matrix constructor, that value is placed in the diagonal of the matrix. For example mat4(1.0) will create a 4 × 4 identity matrix.

  • A matrix can be constructed out of multiple vector arguments, for example a mat2 can be constructed from two vec2s.

  • A matrix can be constructed out of multiple scalar arguments, one for each value in the matrix, consumed from left to right.

The matrix construction is even more flexible than the basic rules just stated in that a matrix can basically be constructed from any combination of scalars and vectors as long as enough components are provided to initialize the matrix. Matrices in OpenGL ES are stored in column major order. When using a matrix constructor, the arguments will be consumed to fill the matrix by column. The comments in the following example show how the matrix constructor arguments will map into columns.

mat3 myMat3 = mat3(1.0, 0.0, 0.0,  // First column
                   0.0, 1.0, 0.0,  // Second column
                   0.0, 1.0, 1.0); // Third column

Vector and Matrix Components

The individual components of a vector can be accessed in two ways: either using the “.” operator or through array subscripting. Depending on the number of components that make up a given vector, each of the components can be accessed through the use of the swizzles {x, y, z, w}, {r, g, b, a}, or {s, t, r, q}. The reason for the three different naming schemes is that vectors are used interchangeably to represent mathematical vectors, colors, and texture coordinates. The x, r, or s component will always refer to the first element of a vector. The different naming conventions are just provided as a convenience. That said, you cannot mix naming conventions when accessing a vector (in other words, you cannot do something like .xgr, as you can only use one naming convention at a time). When using the “.” operator, it is also possible to reorder components of a vector in an operation. The following examples show how this can be done.

vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0}
vec3 temp;

temp = myVec3.xyz;                 // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx;                 // temp = {0.0, 0.0, 0.0}
temp = myVec3.zyx;                 // temp = {2.0, 1.0, 0.0}

In addition to the “.” operator, vectors can also be accessed using the array subscript “[]” operator. In array subscripting, element [0] corresponds to x, element [1] corresponds to y, and so forth. One thing you need to be careful of, though, is that if you access a vector with a non-constant integral expression (e.g., using an integer variable index), then this might not be supported in OpenGL ES 2.0. The reason is because doing dynamic indexing on a vector turns out to be a difficult operation for some hardware, so the OpenGL ES 2.0 spec does not mandate support for this behavior except for on a specific variable type (namely, uniform variables). We mention this to you now just to make you aware of the issue, but we cover the full details on this in Chapter 8, “Vertex Shaders,” when we discuss the limitations of the shading language.

Matrices are treated as being composed of a number of vectors. For example, a mat2 can be thought of as two vec2s, a mat3 as three vec3s, and so forth. For matrices, the individual column is selected using the array subscript operator “[]”, and then each vector can be accessed using the vector access behavior. The following shows some examples of accessing matrices:

mat4 myMat4 = mat4(1.0);    // Initialize diagonal to 1.0 (identity)

vec4 col0 = myMat4[0];      // Get col0 vector out of the matrix
float m1_1 = myMat4[1][1];  // Get element at [1][1] in matrix
float m2_2 = myMat4[2].z;   // Get element at [2][2] in matrix

Constants

It is possible to declare any of the basic types as being constant variables. Constant variables are those whose values do not change within the shader. Constants are declared by adding the const qualifier to the declaration. Const variables must be initialized at declaration time. Some examples of const declarations follow.

const float zero = 0.0;
const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);

Just as in C or C++, a variable that is declared as const is read-only and cannot be modified within the source.

Structures

In addition to the basic types provided in the language, it is also possible to aggregate variables into structures much like in C. The declaration syntax for a structure in the OpenGL ES Shading Language is shown in the following example.

struct fogStruct
{
   vec4  color;
   float start;
   float end;
} fogVar;

The preceding definition will result in a new user type fogStruct and a new variable named fogVar. Structures can be initialized using constructors. After defining a new structure type, a new structure constructor is also defined with the same name of the type. There must be a one-to-one correspondence between types in the structure and those in the constructor. For example, the preceding structure could be initialized using the following construction syntax.

struct fogStruct
{
   vec4  color;
   float start;
   float end;
} fogVar;

fogVar = fogStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
                   0.5,                      // start
                   2.0);                     // end

The constructor for the structure is based on the name of the type and it takes as arguments each of the components. Accessing the elements of a structure is done just as you would a structure in C as shown in the following example.

vec4  color = fogVar.color;
float start = fogVar.start;
float end   = fogVar.end;

Arrays

In addition to structures, the OpenGL ES Shading Language also supports arrays. The syntax is very similar to C, with the arrays being based on a 0 index. The following block of code shows some examples of creating arrays.

float floatArray[4];
vec4  vecArray[2];

There are two important things to note about the use of arrays in the OpenGL ES Shading Language. The first is that many OpenGL ES implementations will not allow an array to be indexed with a variable with an unknown value at compile time. That is, OpenGL ES only mandates that array indexing be supported by constant integral expressions (there is an exception to this, which is the indexing of uniform variables in vertex shaders that is discussed in Chapter 8).

The other note about arrays is that there is no syntax in the OpenGL ES Shading Language to initialize an array at creation time. The elements of the array need to be initialized one-by-one and also arrays cannot be const qualified because there is no syntax for initializing such an array.

These restrictions might at first sound odd to a programmer familiar with C, but the limitations are based on the underlying hardware support for array indexing. It turns out that many GPUs are only built with support for indexing constants (not registers) and this makes it difficult for arbitrary indexing on arrays to be supported.

Operators

Table 5-2 provides a list of the operators that are offered in the OpenGL ES Shading Language.

Table 5-2. OpenGL ES Shading Language Operators

Operator Type

Description

*

Multiply

/

Divide

+

Add

-

Subtract

++

Increment (prefix and postfix)

--

Decrement (prefix and postfix)

=

Assignment

+=, -=, *=, /=

Arithmetic assignment

==, !=, <, >, <=, >=

Comparison operators

&&

Logical and

^^

Logical exclusive or

||

Logical inclusive or

Most of these operators behave just as you are used to in C. As mentioned in the constructor section, the OpenGL ES Shading Language has very strict type rules between operators. That is, the operators must occur between variables that have the same basic type. For the binary operators (*, /, +, -) the basic types of the variables must be floating point or integer. Furthermore, operators such as multiply can operate between combinations of floats, vectors, and matrices. Some examples are provided here.

float myFloat;
vec4  myVec4;
mat4  myMat4;

myVec4 = myVec4 * myFloat;  // Multiplies each component of myVec4
                            // by a scalar myFloat
myVec4 = myVec4 * myVec4;   // Multiplies each component of myVec4
                            // together (e.g., myVec4 ^ 2 )
myVec4 = myMat4 * myVec4;   // Does a matrix * vector multiply of
                            // myMat4 * myVec4
myMat4 = myMat4 * myMat4;   // Does a matrix * matrix multiply of
                            // myMat4 * myMat4
myMat4 = myMat4 * myFloat;  // Multiplies each matrix component by
                            // the scalar myFloat

The comparison operators (==, !=, <, etc.) can only be performed on scalar values. To compare vectors, there are special built-in functions that allow you to do comparisons (more on that later).

Functions

Functions are declared in much the same way as C. If a function is going to be used prior to its definition, then a prototype declaration must be provided. In general, functions work much as you are used to in C. The most significant difference is the way in which parameters are passed to functions. The OpenGL ES Shading Language provides special qualifiers to define whether a variable argument can be modified by the function. These qualifiers are shown in Table 5-3.

Table 5-3. OpenGL ES Shading Language Qualifiers

Qualifier

Description

in

(Default if none specified) This qualifier specifies that the parameter is passed by value and will not be modified by the function.

inout

This qualifier specifies that the variable is passed by reference into the function and if its value is modified it will be changed after function exit.

out

This qualifier says that the variable’s value is not passed into the function, but it will be modified on return from the function.

An example function declaration is provided here. This example shows the use of parameter qualifiers.

vec4 myFunc(inout float myFloat, // inout parameter
            out vec4 myVec4,     // out parameter
            mat4 myMat4);        // in parameter (default)

An example function definition is given here for a simple function that computes basic diffuse lighting.

vec4 diffuse(vec3 normal,
             vec3 light,
             vec4 baseColor)
{
   return baseColor * dot(normal, light);
}

One note about functions in the OpenGL ES Shading Language is that functions cannot be recursive. The reason for this limitation is that some implementations will implement function calls by actually making the function code inline in the final generated program for the GPU. The shading language was purposely structured to allow this sort of an inline implementation to enable GPUs that do not have a stack and flow control.

Built-In Functions

The preceding section described how a shader author creates a function. One of the most powerful features of the OpenGL ES Shading Language is the built-in functions that are provided in the language. As an example, here is some shader code for computing basic specular lighting in a fragment shader.

float nDotL = dot(normal , light);
float rDotV = dot(viewDir, (2.0 * normal) * nDotL – light);
float specular = specularColor * pow(rDotV, specularPower);

As you can see, this block of shader code use the dot built-in function to compute the dot product between two vectors and the pow built-in function to raise a scalar to a power. These are just two simple examples; there is a wide array of built-in functions in the OpenGL ES Shading Language for various computational tasks that one typically has to do in a shader. In Appendix B we provide a complete reference to the built-in functions provided in the OpenGL ES Shading Language. For now, we just want to make you aware that there are a lot of built-in functions in the language and to be proficient in writing shaders you are going to want to familiarize yourself with the most common ones.

Control Flow Statements

The syntax for control flow statements in the OpenGL ES Shading Language is similar to C. Simple if-then-else logical tests can be done using the same syntax as C. For example:

if(color.a < 0.25)
{
   color *= color.a;
}
else
{
   color = vec4(0.0);
}

The expression that is being tested in the conditional statement must evaluate to a boolean. That is, the test must either be based on a boolean value or some expression that evaluates to a boolean (e.g., a comparison operator). This is the basics of how conditionals can be expressed in the OpenGL ES Shading Language.

In addition to basic if-then-else statements, it is also possible to write simple for loops. There are a variety of restrictions placed on the types of loops supported in the OpenGL ES Shading Language. These restrictions have to do with the expected limitations of some of the OpenGL ES 2.0 hardware devices. To boil it down to its simplest form, for loops in OpenGL ES must have an iteration count that is known at compile time. An example of a for loop that would be supported in OpenGL ES is shown here.

for(int i = 0; i < 3; i++)
{
   sum += i;
}

You should generally tread with caution when using loops in OpenGL ES. The basic restrictions are as follows: there must be only one loop iteration variable and it must be incremented or decremented using a simple statement (i++, i--, i+=constant, i-=constant); the stop condition must be a comparison between the loop index and a constant expression; and you must not change the value of the iterator in the loop. Essentially, the OpenGL ES Shading Language does not require hardware to provide looping support.

Rather, it restricts the loops that can be expressed to those that can be unrolled by the compiler.

Some examples of loops that would not be supported in OpenGL ES are given here.

float myArr[4];
for(int i = 0; i < 3; i++)
{
   sum += myArr[i]; // NOT ALLOWED IN OPENGL ES, CANNOT DO
                    // INDEXING WITH NONCONSTANT EXPRESSION
}

...

uniform int loopIter;

// NOT ALLOWED IN OPENGL ES, loopIter ITERATION COUNT IS NONCONSTANT
for(int i = 0; i < loopIter; i++)
{
   sum += i;
}

These restrictions on loops might seem quite odd to a programmer used to the flexibility provided by CPUs. However, remember that OpenGL ES is an API for embedded systems and the specification authors intended to enable GPUs to use small area and power budgets. Flow control and looping tends to be a difficult operation for GPUs, so these restrictions on loops allow for simplified GPU implementations. These sorts of restrictions are also common on older generation desktop GPUs such as the ATI Radeon 9500, which did not support loops in the fragment shader.

Uniforms

One of the variable type modifiers in the OpenGL ES Shading Language is the uniform variable. Uniform variables are variables that store read-only values that are passed in by the application through the OpenGL ES 2.0 API to the shader. Uniforms are useful for storing all kinds of data that shaders need, such as transformation matrices, light parameters, or colors. Basically, any parameter to a shader that is constant across either all vertices or fragments (but that is not known at compile time) should be passed in as a uniform.

Uniform variables are declared at the global scope and simply require the uniform qualifier. Some examples of uniform variables are shown here.

uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform vec3 lightPosition;

In Chapter 4, we described how an application loads uniform variables to a shader. Note also that the namespace for uniform variables is shared across both a vertex and a fragment shader. That is, if a vertex and fragment shader are linked together into a program object, they share the same set of uniform variables. Therefore, if a uniform variable is declared in the vertex shader and also in the fragment shader, its declaration must match. When the application loads the uniform variable through the API, its value will be available in both the vertex and fragment shaders.

Another note about uniforms is that uniform variables generally are stored in hardware into what is known as the “constant store.” This is a special space allocated in the hardware for the storage of constant values. Because this storage is typically of a fixed size, there is a limit on the number of uniforms that can be used in a program. This limitation can be determined by reading the value of the gl_MaxVertexUniformVectors and gl_MaxFragmentUniformVectors built-in variables (or by querying GL_MAX_VERTEX_UNIFORM_VECTORS or GL_MAX_FRAGMENT_UNIFORM_VECTORS using glGetIntegerv). An implementation of OpenGL ES 2.0 must provide at least 128 vertex uniform vectors and 16 fragment uniform vectors, although it is free to provide more. We cover the full set of limitations and queries available for the vertex and fragment shaders in Chapter 8, “Vertex Shaders,” and Chapter 10, “Fragment Shaders.”

Attributes

Another special variable type in the OpenGL ES Shading Language is the attribute variable. Attribute variables are available only in the vertex shader and are used to specify the per-vertex inputs to the vertex shader. Attributes typically store data such as positions, normals, texture coordinates, and colors. The key here to understand is that attributes are data that are specified for each vertex being drawn. It is really up to the user of the shader to determine what data belongs in the attributes. Example 5-1 is a sample vertex shader that has a position and texture coordinate attribute.

Example 5-1. Sample Vertex Shader

uniform mat4 u_matViewProjection;
attribute vec4 a_position;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord;

void main(void)
{
   gl_Position = u_matViewProjection * a_position;
   v_texCoord = a_texCoord0;
}

The two vertex attributes in this shader a_position and a_texCoord0 will be loaded with data by the application. Essentially, the application will create a vertex array that contains a position and a texture coordinate for each vertex. We explain this process in full detail in Chapter 6, “Vertex Attributes, Vertex Arrays, and Buffer Objects.” For now, just make sure you understand that attributes are the per-vertex inputs to the vertex shader.

As with uniform variables, the underlying hardware typically has limits on the number of attribute variables that can be input to a vertex shader. The maximum number of attributes that an implementation supports is given by the gl_MaxVertexAttribs built-in variable (or by querying for GL_MAX_VERTEX_ATTRIBS using glGetIntegerv). The minimum number of attributes that an OpenGL ES 2.0 implementation can support is eight. Implementations are free to support more, but if you want to write shaders that are guaranteed to run on any OpenGL ES 2.0 implementation you should restrict yourself to using no more than eight attributes. We cover attribute limitations in more detail in Chapter 8.

Varyings

The final variable type modifier in the OpenGL ES Shading Language that we describe is a varying. Varying variables are used to store the output of the vertex shader and also the input of a fragment shader. Basically, each vertex shader will output the data it needs to pass the fragment shader into one or more varying variables. These variables will then also be declared in the fragment shader (with matching types) and will be linearly interpolated across the primitive during rasterization (if you want more details on how this interpolation occurs during rasterization, jump to Chapter 6 on rasterization).

Some examples of varying declarations are as follows:

varying vec2 texCoord;
varying vec4 color;

The varying declarations will be put in both the vertex and fragment shader. As mentioned, the varyings are the output of the vertex shader and input to the fragment shader, so they must be declared identically. As with uniforms and attributes, the underlying hardware typically has a limit to the number of varyings (on the hardware, these are usually referred to as interpolators). The number of varyings supported by an implementation is given by the gl_MaxVaryingVectors built-in variable (or by querying for GL_MAX_VARYING_VECTORS using glGetIntegerv). The minimum number of varying vectors that an implementation of OpenGL ES 2.0 can support is eight.

Example 5-2 is a sample of a vertex shader and a fragment shader with matching varying declarations.

Example 5-2. Vertex and Fragment Shader with Matching Varying Declarations

// Vertex shader
uniform mat4 u_matViewProjection;
attribute vec4 a_position;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord; // Varying in vertex shader

void main(void)
{
   gl_Position = u_matViewProjection * a_position;
   v_texCoord = a_texCoord0;
}

// Fragment shader
precision mediump float;
varying vec2 v_texCoord; // Varying in fragment shader

uniform sampler2D s_baseMap;
uniform sampler2D s_lightMap;
void main()
{
   vec4 baseColor;
   vec4 lightColor;

   baseColor = texture2D(s_baseMap, v_texCoord);
   lightColor = texture2D(s_lightMap, v_texCoord);
   gl_FragColor = baseColor * (lightColor + 0.25);
}

Preprocessor and Directives

One feature of the OpenGL ES Shading Language we have not mentioned yet is the preprocessor. The OpenGL ES Shading Language features a preprocessor that follows many of the conventions of a standard C++ preprocessor. Macros can be defined and conditional tests can be performed using the following directives:

#define
#undef
#if
#ifdef
#ifndef
#else
#elif
#endif

Note that macros cannot be defined with parameters (as they can be in C++ macros). The #if, #else, and #elif can use the defined test to see whether a macro is defined. The following macros are predefined and their description is given next:

__LINE__     // Replaced with the current line number in a shader
__FILE__     // Always 0 in OpenGL ES 2.0
__VERSION__  // The OpenGL ES shading language version (e.g., 100)
GL_ES        // This will be defined for ES shaders to a value of 1

The #error directive will cause a compilation error to occur during shader compilation with a message placed in the info log. The #pragma directive is used to specify implementation-specific directives to the compiler.

A new directive (not in C++) that was added to the preprocessor is #version. For OpenGL ES 2.0 shaders, this value should be set to 100. This directive sets the shader version with which the shader should be compiled. The purpose of this directive is that when future versions of the API add new features to the shading language, the version token can be used to make sure a shader is compiled to the language version it is written against. This token must occur at the beginning of the source code. The following shows the proper usage of the #version directive:

#version 100 // OpenGL ES Shading Language v1.00

Another important directive in the preprocessor is #extension, which is used to enable and set the behavior of extensions. When vendors (or groups of vendors) extend the OpenGL ES Shading Language, they will create a language extension specification (e.g., GL_OES_texture_3D). The shader must instruct the compiler whether or not to allow extensions to be used, and if not, what behavior should occur. This is done using the #extension directive. The general format of #extension usage is shown in the following code.

// Set behavior for an extension
#extension extension_name : behavior
// Set behavior for ALL extensions
#extension all : behavior

The first argument will be either the name of the extension (e.g., GL_OES_texture_3D) or all, which means that the behavior applies to all extensions. The behavior has four possible options, as shown in Table 5-4.

Table 5-4. Extension Behaviors

Extension Behavior

Description

require

The extension is required, so the preprocessor will throw an error if the extension is not supported. If all is specified, this will always throw an error.

enable

The extension is enabled, so the preprocessor will warn if the extension is not supported. The language will be processed as if the extension is enabled. If all is specified, this will always throw an error.

warn

Warn on any use of the extension, unless that use is required by another enabled extension. If all is specified, there will be a warning thrown whenever the extension is used. Also, a warning will be thrown if the extension is not supported.

disable

The extension is disabled, so errors will be thrown if the extension is used. If all is specified (this is specified by default), no extensions are enabled.

As an example, if you want the preprocessor to produce a warning if the 3D texture extension is not supported (and the shader to be processed as if it is supported), you would add the following at the top of your shader:

#extension GL_OES_texture_3D : enable

Uniform and Varying Packing

As noted in the preceding sections on uniforms and varyings, there is a fixed number of underlying hardware resources available for the storage of each variable. Uniforms are typically stored in what is known as the constant store, which can be thought of as a physical array of vectors. Varyings are typically stored in interpolators, which again are usually stored as an array of vectors. As you’ve probably noticed, shaders can declare uniforms and varyings of various types including scalars, various vector components, and matrices. The question arises of how these various variable declarations map to the physical space that’s available on the hardware. In other words, if an OpenGL ES 2.0 implementation says it supports eight varying vectors, how does the physical storage actually get used?

The way that this is handled in OpenGL ES 2.0 is that there are packing rules that define how the varyings and uniforms will map to physical storage space. The rules for packing are based on the notion that the physical storage space is organized into a grid with four columns (one column for each vector component) and a row for each storage location. The packing rules seek to pack variables such that the complexity of the generated code will remain constant. In other words, the packing rules will not do reordering that will require the compiler to generate extra instructions to merge unpacked data. Rather, the packing rules seek to optimize the use of the physical address space without negatively impacting runtime performance.

Let’s take a look at an example group of uniform declarations and how these would be packed.

uniform mat3 m;
uniform float f[6];
uniform vec3 v;

If no packing were done at all, you can see that a lot of constant storage space would be wasted. The matrix m would take up three rows, the array f would take up six rows, and the vector v would take up one row. This would use a total of 10 rows to store the variables. Figure 5-1 shows what the results would be without any packing.

Table 5-1. Uniform Storage without Packing

Location

X

Y

Z

W

0

M[0].x

m[0].y

m[0].z

-

1

M[1].x

m[1].y

m[1].z

-

2

M[2].x

m[2].y

m[2].z

-

3

f[0]

-

-

-

4

f[1]

-

-

-

5

f[2]

-

-

-

6

f[3]

-

-

-

7

f[4]

-

-

-

8

f[5]

-

-

-

9

v.x

v.y

v.z

-6

With the packing rules, the variables will get organized such that they pack into the grid as shown in Figure 5-2.

Table 5-2. Uniform Storage with Packing

Location

X

Y

Z

W

0

M[0].x

m[0].y

m[0].z

f[0]

1

M[1].x

m[1].y

m[1].z

f[1]

2

M[2].x

m[2].y

m[2].z

f[2]

3

v.x

v.y

v.z

f[3]

4

-

-

-

f[4]

5

-

-

-

f[5]

With the packing rules, only six physical constant locations need to be used. You will notice that the array f needs to keep its elements spanning across row boundaries. The reason for this is because typically GPUs index the constant store by vector location index. The packing must keep the arrays spanning across row boundaries so that indexing will still work.

All of the packing that is done is completely transparent to the user of the OpenGL ES Shading Language except for one detail: it impacts the way in which uniforms and varyings are counted. If you want to write shaders that are guaranteed to run on all implementations of OpenGL ES 2.0, you should not use more uniforms or varyings than would exceed the minimum allowed storage sizes after packing. For this reason, it’s important to be aware of packing so that you can write portable shaders that will not exceed the minimum allowed storage on any implementation of OpenGL ES 2.0.

Precision Qualifiers

One notable new feature addition to OpenGL ES that differs from desktop OpenGL is the introduction of precision qualifiers to the shading language. Precision qualifiers enable the shader author to specify the precision with which computations for a shader variable are performed. Variables can be declared to have either low, medium, or high precision. These qualifiers are used as hints to the compiler to allow it to perform computations with variables at a potentially lower range and precision. It is possible that at lower precisions, some implementations of OpenGL ES might either be able to run the shaders faster or with better power efficiency. Of course, that efficiency savings comes at the cost of precision, which can result in artifacts if precision qualifiers are not used properly. Note that there is nothing in the OpenGL ES specification that says that multiple precisions must be supported in the underlying hardware, so it is perfectly valid for an implementation of OpenGL ES to do all calculations at the highest precision and simply ignore the qualifiers. However, on some implementations using a lower precision might be an advantage.

Precision qualifiers can be used to specify the precision of any float or integer-based variable. The keywords for specifying the precision are lowp, mediump, and highp. Some examples of declarations with precision qualifiers are shown here.

highp vec4 position;
varying lowp vec4 color;
mediump float specularExp;

In addition to precision qualifiers, there is also the notion of default precision. That is, if a variable is declared without having a precision qualifier, it will have the default precision for that type. The default precision qualifier is specified at the top of a vertex or fragment shader using the following syntax:

precision highp float;
precision mediump int;

The precision specified for float will be used as the default precision for all variables based on a floating-point value. Likewise, the precision specified for int will be used as the default precision for all integer-based variables.

In the vertex shader, if no default precision is specified, then the default precision for int and float is both highp. That is, all variables declared without a precision qualifier in a vertex shader will have the highest precision. The rules for the fragment shader are different. In the fragment shader, there is no default precision given for floats: Every shader must declare a default float precision or specify the precision for every float variable. In addition, OpenGL ES 2.0 does not require that an implementation support high precision in the fragment shader. The way to determine whether high precision is supported in the fragment shader is whether the GL_FRAGMENT_PRECISION_HIGH preprocessor macro is defined (in addition, the implementation will export the OES_fragment_precision_high extension string).

In this book we often include the following code at the top of a fragment shader:

#ifdef GL_FRAGMENT_PRECISION_HIGH
   precision highp float;
#else
   precision mediump float;
#endif

This makes sure that whether the implementation supports medium or high precision, the shader will compile.

One final note is that the precision specified by a precision qualifier has an implementation-dependent range and precision. There is an associated API call for determining the range and precision for a given implementation, which is covered in Chapter 14, “State Queries.”

Invariance

The final topic we’ll discuss in the shading language is invariance. There is a keyword introduced in the OpenGL ES Shading Language invariant that can be applied to any varying output of a vertex shader. What do we mean by invariance and why is this necessary? The issue is that shaders are compiled and the compiler might perform optimizations that cause instructions to be reordered. This instruction reordering means that equivalent calculations between two shaders are not guaranteed to produce exactly identical results. This can be an issue in particular for multipass shader effects where the same object is being drawn on top of itself using alpha blending. If the precision of the values used to compute the output position are not exactly identical, then artifacts can exist due to the precision differences. This usually manifests itself as Z fighting, or when small Z precision differences per pixel cause the different passes to shimmer against each other.

The following example demonstrates visually why invariance is important to get right when doing multipass shading. The following torus object is drawn in two passes: The fragment shader computes specular lighting in the first pass and ambient and diffuse lighting in the second pass. The vertex shaders do not use invariance so small precision differences cause the Z fighting as shown in Figure 5-3.

Z Fighting Artifacts Due to Not Using Invariance

Figure 5-3. Z Fighting Artifacts Due to Not Using Invariance

The same multipass vertex shaders using invariance for position produce the correct image in Figure 5-4.

Z Fighting Avoided Using Invariance

Figure 5-4. Z Fighting Avoided Using Invariance

The introduction of invariance gives the shader writer a way to specify that if the same computations are used to compute an output, its value must be exactly the same (or invariant). The invariant keyword can be used either on varying declarations or for varyings that have already been declared. Some examples follow.

invariant gl_Position;
invariant varying texCoord;

Once invariance is declared for an output, the compiler guarantees that the results will be the same given the same computations and inputs into the shader. For example, given you have two vertex shaders that compute output position by multiplying the view projection matrix by the input position, you are guaranteed that those positions will be invariant.

uniform mat4 u_viewProjMatrix;
attribute vec4 a_vertex;
invariant gl_Position;
void main
{
   // ...
   gl_Position = u_viewProjMatrix * a_vertex; // Will be the same
                                              // value in all
                                              // shaders with the
                                              // same viewProjMatrix
                                              // and vertex
}

It is also possible to make all variables globally invariant using a #pragma directive.

#pragma STDGL invariant(all)

One word of caution: Because the compiler needs to guarantee invariance, it might have to limit the optimizations it does. Therefore, the invariant qualifier should be used only when necessary; otherwise it might result in performance degradation. For this reason, the #pragma to globally enable invariance should only be used when invariance is really required for all variables. Note also that while invariance does imply that the calculation will have the same results on a given GPU, it does not mean that the computation would be invariant across any implementation of OpenGL ES.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset