Shader Fundamentals
Shader Fundamentals
• The uniform location remains valid until you link the program
again, so there is no need to call glGetUniformLocation every
frame.
• GLfloat fValue = 45.2f;
• glUniform1fv(iLocation, 1, &fValue);
• layout (location = 0) uniform float fTime;
• layout (location = 1) uniform int iIndex;
• layout (location = 2) uniform vec4 vColorValue;
• layout (location = 3) uniform bool bSomeFlag;
• glUseProgram(myShader);
• glUniform1f(0, 45.2f);
• glUniform1i(1, 42);
• glUniform4f(2, 1.0f, 0.0f, 0.0f, 1.0f);
• glUniform1i(3, GL_FALSE);
• uniform vec4 vColor;
uniform TransformBlock
{
float scale; // Global scale to apply to everything
vec3 translation; // Translation in X, Y, and Z
float rotation[3]; // Rotation around X, Y, and Z axes
mat4 projection_matrix; // A generalized projection matrix to apply
// after scale and rotate
} transform;
• Inside the shader, you can refer to the members of the block using its
instance name, transform (for example, transform.scale or
transform.projection_matrix).
Varying Qualifier
• Variables that are passed from vertex shader
to fragment shader
• Automatically interpolated by the rasterizer
• Old style used the varying qualifier
varying vec4 color;
• Now use out in vertex shader and in in the
fragment shader
out vec4 color;
Vertex Attributes
• Vertex attributes are used to communicate from
"outside" to the vertex shader. Unlike uniform
variables, values are provided per vertex (and not
globally for all vertices).
• There are built-in vertex attributes like the
normal or the position, or you can specify your
own vertex attribute like a tangent or another
custom value.
• Attributes can't be defined in the fragment
shader.
Built in Vertex Attributes
• In the C++ program you can use the regular OpenGL function to set
vertex attribute values, for example glVertex3f for the position.
• gl_Vertex Position (vec4)
• gl_Normal Normal (vec4)
• gl_Color Primary color of vertex (vec4)
• gl_MultiTexCoord0 Texture coordinate of texture unit 0 (vec4)
• gl_MultiTexCoord1 Texture coordinate of texture unit 1 (vec4)
• ….
• gl_MultiTexCoord7 Texture coordinate of texture unit 7 (vec4)
• gl_FogCoord Fog Coord (float)
Example: Built-in Vertex Attributes
glBegin(GL_TRIANGLES)
glVertex3f(0.0f, 0.0f, 0.0f);
glColor3f(0.1,0.0,0.0);
glVertex3f(1.0f, 0.0f, 0.0f);
glColor3f(0.0,0.1,0.0);
glVertex3f(1.0f, 1.0f, 0.0f);
glColor3f(0.1,0.1,0.0);
glEnd();
• Vertex Shader Source Code
void main(void) {
vec4 a = gl_Vertex + gl_Color;
gl_Position = gl_ModelViewProjectionMatrix * a;
}
• Fragment Shader Source Code
void main (void) {
gl_FragColor = vec4(0.0,0.0,1.0,1.0);
}
// gl_FragColor is deprecated in GLSL 1.3 (OpenGL 3.0)
Custom Vertex Attributes
• A custom, user-defined attribute can also be defined.
• The OpenGL function glBindAttribLocation associates
the name of the variable with an index.
• For example, glBindAttribLocation(ProgramObject, 10,
"myAttrib") would bind the attribute "myAttrib" to
index 10.
• The maximum number of attribute locations is limited
by the graphics hardware. You can retrieve the
maximum supported number of vertex attributes with
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &n).
• Setting attribute values can be done
using glVertexAttrib function.
• Unfortunately there are certain limitations when using
this on NVidia Hardware. In other words, NVidia
hardware indices are reserved for built-in attributes:
• gl_Vertex 0
• gl_Normal 2
• gl_Color 3
• gl_SecondaryColor 4
• gl_FogCoord 5
• gl_MultiTexCoord0 8
• …..
• gl_MultiTexCoord7 15
Process Overview
Our first vertex shader
#version 450 core
void main(void)
{
gl_Position = vec4(0.0, 0.0, 0.5, 1.0);
}
• Inside our main function, we assign a value to
gl_Position. All variables that start with gl_ are
part of OpenGL and connect shaders to each
other or to the various parts of fixed functionality
in OpenGL.
• In the vertex shader, gl_Position represents the
output position of the vertex. The value we assign
(vec4(0.0, 0.0, 0.5, 1.0)) places the vertex right in
the middle of OpenGL’s clip space, which is the
coordinate system expected by the next stage of
the OpenGL pipeline.
Fragment shader
#version 450 core
out vec4 color;
void main(void) {
color = vec4(0.0, 0.8, 1.0, 1.0);
}
• It starts with a #version 450 core declaration.
• Next, it declares color as an output variable
using the out keyword.
• In fragment shaders, the value of output
variables will be sent to the window or screen.
GLuint compile_shaders(void) {
GLuint vertex_shader;
GLuint fragment_shader;
GLuint program;
// Source code for vertex shader
static const GLchar * vertex_shader_source[] =
{
"#version 450 core \n"
" \n"
"void main(void) \n"
"{ \n"
" gl_Position = vec4(0.0, 0.0, 0.5, 1.0); \n"
"} \n"
};
static const GLchar * fragment_shader_source[] =
{
"#version 450 core \n"
" \n"
"out vec4 color; \n"
" \n"
"void main(void) \n"
"{ \n"
" color = vec4(0.0, 0.8, 1.0, 1.0); \n"
"} \n"
};
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, vertex_shader_source, NULL);
glCompileShader(vertex_shader);
// Create and compile fragment shader
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, fragment_shader_source, NULL);
glCompileShader(fragment_shader);
// Create program, attach shaders to it, and link it
program = glCreateProgram();
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);
// Delete the shaders as the program has them now
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
return program;
}
• glCreateShader() creates an empty shader object, ready to
accept source code and be compiled.
• glShaderSource() hands shader source code to the shader
object so that it can keep a copy of it.
• glCompileShader() compiles whatever source code is
contained in the shader object.
• glCreateProgram() creates a program object to which you
can attach shader objects.
• glAttachShader() attaches a shader object to a program
object.
• glLinkProgram() links all of the shader objects attached to a
program object together.
• glDeleteShader() deletes a shader object. Once a shader
has been linked into a program object, the program
contains the binary code and the shader is no longer
needed.
void startup() {
rendering_program = compile_shaders();
glCreateVertexArrays(1, &vertex_array_object);
glBindVertexArray(vertex_array_object);
}
void shutdown() {
glDeleteVertexArrays(1, &vertex_array_object);
glDeleteProgram(rendering_program);
}
• The compile_shaders function returns the newly
created program object. When we call this function, we
need to keep the returned program object somewhere
so that we can use it to draw things.
• Also, we really don’t want to recompile the whole
program every time we want to use it. So, we need a
function that is called once when the program starts
up.
• The sb7 application framework provides just such a
function: application::startup(), which we can override
in our sample application and use to perform any one-
time setup work.
• One final thing that we need to do before we can draw
anything is to create a vertex array object (VAO), which
is an object that represents the vertex fetch stage of
the OpenGL pipeline and is used to supply input to the
vertex shader.
• As our vertex shader doesn’t have any inputs right now,
we don’t need to do much with the VAO.
• Nevertheless, we still need to create the VAO so that
OpenGL will let us draw.
• To create the VAO, we call the OpenGL function
glCreateVertexArrays(); to attach it to our context, we
call glBindVertexArray().
• void glCreateVertexArrays(GLsizei n, GLuint * arrays);
• void glBindVertexArray(GLuint array);
• We modify our render() function to call glUseProgram() to
tell OpenGL to use our program object for rendering and
then call our first drawing command, glDrawArrays().
glDrawArrays(GL_POINTS, 0, 1);
• You may have noticed the layout (location =0) code in the
declaration of the offset attribute. This is a layout qualifier,
which we have used to set the location of the vertex
attribute to zero. This location is the value we’ll pass in
index to refer to the attribute.
• Each time we call one of the glVertexAttrib*()
function, it will update the value of the vertex
attribute that is passed to vertex shader.
virtual void render(double currentTime) {
const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f,
(float)cos(currentTime) * 0.5f + 0.5f,
0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, color);
// Use the program object we created earlier for rendering
glUseProgram(rendering_program);
GLfloat attrib[] = { (float)sin(currentTime) * 0.5f,
(float)cos(currentTime) * 0.6f,
0.0f, 0.0f };
// Update the value of input attribute 0
glVertexAttrib4fv(0, attrib);
// Draw one triangle
glDrawArrays(GL_TRIANGLES, 0, 3); }
Passing Data from Stage to Stage
• Anything you write to an output variable in one
shader is sent to a similarly named variable
declared with the in keyword in the subsequent
stage.
• For example, if your vertex shader declares a
variable called vs_color using the out keyword, it
would match up with a variable named vs_color
declared with the in keyword in the fragment
shader stage (assuming no other stages were
active in between)
Vertex shader with an output
#version 450 core
// 'offset' and 'color' are input vertex attributes
layout (location = 0) in vec4 offset;
layout (location = 1) in vec4 color;
// 'vs_color' is an output that will be sent to the next shader stage
out vec4 vs_color;
void main(void) {
const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4(0.25, 0.25, 0.5, 1.0));
// Add 'offset' to our hard-coded vertex position
gl_Position = vertices[gl_VertexID] + offset;
// Output a fixed value for vs_color
vs_color = color;
}
Fragment shader with an input
#version 450 core
// Input from the vertex shader
in vec4 vs_color;
// Output to the framebuffer
out vec4 color;
void main(void) {
// Simply assign the color we were given by the
vertex shader to our output
color = vs_color;
}
Interface Blocks
• In most nontrivial applications, you will likely
want to communicate a number of different
pieces of data between stages; these may include
arrays, structures, and other complex
arrangements of variables.
• To achieve this, we can group together a number
of variables into an interface block. The
declaration of an interface block looks a lot like a
structure declaration, except that it is declared
using the in or out keyword.
………
// Declare VS_OUT as an output interface block
out VS_OUT {
vec4 color; // Send color to the next stage
} vs_out;
void main(void) {
…..
vs_out.color = color;
}
• Note that the interface block has both a block name
(VS_OUT, uppercase) and an instance name (vs_out,
lowercase).
• Interface blocks are matched between stages using the
block name (VS_OUT in this case), but are referenced in
shaders using the instance name.
#version 450 core
// Declare VS_OUT as an input interface block
in VS_OUT {
vec4 color;
} fs_in;
// Output to the framebuffer
out vec4 color;
void main(void) {
// Simply assign the color we were given by the vertex shader to
our output
color = fs_in.color;
}
• Note that interface blocks are only for moving
data from shader stage to shader stage—you
can’t use them to group together inputs to the
vertex shader or outputs from the fragment
shader.
Our first geometry shader
#version 450 core
layout (triangles) in;
layout (points, max_vertices = 3) out;
void main(void) {
int i;
for (i = 0; i < gl_in.length(); i++) {
gl_Position = gl_in[i].gl_Position;
EmitVertex();
}}
• The shader converts triangles into points so that we
can see their vertices.
• The first layout qualifier indicates that the geometry
shader is expecting to see triangles as its input.
• The second layout qualifier tells OpenGL that the
geometry shader will produce points and that the
maximum number of points that each shader will
produce will be three.
• In the main function, a loop runs through all of the
members of the gl_in array, which is determined by
calling its .length() function.
• We actually know that the length of the array will be three because
we are processing triangles and every triangle has three vertices.
• The outputs of the geometry shader are again similar to those of a
vertex shader.
• In particular, we write to gl_Position to set the position of the
resulting vertex.
• Next, we call EmitVertex(), which produces a vertex at the output of
the geometry shader.
• Geometry shaders automatically call EndPrimitive() at the end of
your shader, so calling this function explicitly is not necessary in this
example.
• As a result of running this shader, three vertices will be produced
and rendered as points.
Deriving a fragment’s color from its position
void main(void)
{
……
}
Multiple separate vertex attributes
GLuint buffer[2];
GLuint vao;
static const GLfloat positions[] = { ... };
static const GLfloat colors[] = { ... };
// Create the vertex array object
glCreateVertexArrays(1, &vao)
// Get create two buffers
glCreateBuffers(2, &buffer[0]);
// Initialize the first buffer
glNamedBufferStorage(buffer[0], sizeof(positions), positions, 0);
// Bind it to the vertex array - offset zero, stride = sizeof(vec3)
glVertexArrayVertexBuffer(vao, 0, buffer[0], 0, sizeof(vmath::vec3));
// Tell OpenGL what the format of the attribute is
glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
// Tell OpenGL which vertex buffer binding to use for this attribute
glVertexArrayAttribBinding(vao, 0, 0);
// Enable the attribute
glEnableVertexArrayAttrib(vao, 0);
// Perform similar initialization for the second buffer
glNamedBufferStorage(buffer[1], sizeof(colors), colors, 0);
glVertexArrayVertexBuffer(vao, 1, buffer[1], 0,
sizeof(vmath::vec3));
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(vao, 1, 1);
glEnableVertexAttribArray(1);
Multiple interleaved vertex attributes
struct vertex
{
// Position
float x;
float y;
float z;
// Color
float r;
float g;
float b;
};
GLuint vao;
GLuint buffer;
static const vertex vertices[] = { ... };
// Create the vertex array object
glCreateVertexArrays(1, &vao);
// Allocate and initialize a buffer object
glCreateBuffers(1, &buffer);
glNamedBufferStorage(buffer, sizeof(vertices),
vertices, 0);
// Set up two vertex attributes - first positions
glVertexArrayAttribBinding(vao, 0, 0);
glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE,
offsetof(vertex, x));
glEnableVertexArrayAttrib(0);
// Now colors
glVertexArrayAttribBinding(vao, 1, 0);
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE,
offsetof(vertex, r));
glEnableVertexArrayAttrib(1);
// Finally, bind our one and only buffer to the vertex array
object
glVertexArrayVertexBuffer(vao, 0, buffer);
Spinning cube
// First create and bind a vertex array object
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
static const GLfloat vertex_positions[] = {
-0.25f, 0.25f, -0.25f,
-0.25f, -0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
0.25f, 0.25f, -0.25f,
-0.25f, 0.25f, -0.25f,
….
….
-0.25f, 0.25f, -0.25f,
0.25f, 0.25f, -0.25f,
0.25f, 0.25f, 0.25f,
0.25f, 0.25f, 0.25f,
-0.25f, 0.25f, 0.25f,
-0.25f, 0.25f, -0.25f
};
// Now generate some data and put it in a buffer object
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER,
sizeof(vertex_positions), vertex_positions,
GL_STATIC_DRAW);
void main(void) {
color = fs_in.color;
}
A few frames from the spinning cube
application
Rendering loop for 24 spinning cubes
// Clear the framebuffer with dark green and clear
// the depth buffer to 1.0
glClearBufferfv(GL_COLOR, 0, sb7::color::Green);
glClearBufferfi(GL_DEPTH_STENCIL, 0, 1.0f, 0);
// Activate our program
glUseProgram(program);
// Set the model-view and projection matrices
glUniformMatrix4fv(proj_location, 1, GL_FALSE,
proj_matrix);
for (i = 0; i < 24; i++) {
// Calculate a new model-view matrix for each one
float f = (float)i + (float)currentTime * 0.3f;
vmath::mat4 mv_matrix =
vmath::translate(0.0f, 0.0f, -20.0f) *
vmath::rotate((float)currentTime * 45.0f, 0.0f, 1.0f, 0.0f) *
vmath::rotate((float)currentTime * 21.0f, 1.0f, 0.0f, 0.0f) *
vmath::translate(sinf(2.1f * f) * 2.0f,
cosf(1.7f * f) * 2.0f,
sinf(1.3f * f) * cosf(1.5f * f) * 2.0f);
// Update the uniform
glUniformMatrix4fv(mv_location, 1, GL_FALSE, mv_matrix);
// Draw - notice that we haven't updated the projection
matrix
glDrawArrays(GL_TRIANGLES, 0, 36);
}
Setting up indexed cube geometry
static const GLfloat vertex_positions[] = {
-0.25f, -0.25f, -0.25f,
-0.25f, 0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
0.25f, 0.25f, -0.25f,
0.25f, -0.25f, 0.25f,
0.25f, 0.25f, 0.25f,
-0.25f, -0.25f, 0.25f,
-0.25f, 0.25f, 0.25f, };
static const GLushort vertex_indices[] = {
0, 1, 2,
2, 1, 3,
2, 3, 4,
4, 3, 5,
4, 5, 6,
6, 5, 7,
6, 7, 0,
0, 7, 1,
6, 0, 2,
2, 4, 6,
7, 5, 3,
7, 3, 1 };
glGenBuffers(1, &position_buffer);
glBindBuffer(GL_ARRAY_BUFFER, position_buffer);
glBufferData(GL_ARRAY_BUFFER,
sizeof(vertex_positions),
vertex_positions,
GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);
glGenBuffers(1, &index_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
sizeof(vertex_indices),
vertex_indices,
GL_STATIC_DRAW);
Drawing indexed cube geometry
// Clear the framebuffer with dark green
static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, green);
// Activate our program
glUseProgram(program);
// Set the model-view and projection matrices
glUniformMatrix4fv(mv_location, 1, GL_FALSE, mv_matrix);
glUniformMatrix4fv(proj_location, 1, GL_FALSE, proj_matrix);
// Draw 6 faces of 2 triangles of 3 vertices each = 36 vertices
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, 0);
The Base Vertex
• The first advanced version of glDrawElements()
that takes an extra parameter is
glDrawElementsBaseVertex(), whose prototype
is:
// Free the memory we allocated before - OpenGL now has our data
delete [] data;
Reading from a texture in GLSL
#version 450 core
uniform sampler2D s;
out vec4 color;
void main(void) {
color = texelFetch(s, ivec2(gl_FragCoord.xy), 0);
}
• Once you’ve created a texture object and placed some
data in it, you can read that data in your shaders and
use it to color fragments, for example. Textures are
represented in shaders as sampler variables and are
hooked up to the outside world by declaring uniforms
with sampler types.
• The sampler type that represents two-dimensional
textures is sampler2D. To access our texture in a
shader, we can create a uniform variable with the
sampler2D type, and then use the texelFetch built-in
function with that uniform and a set of texture
coordinates at which to read from the texture
Vertex shader with a single texture coordinate
#version 450 core
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (location = 0) in vec4 position;
layout (location = 4) in vec2 tc;
out VS_OUT {
vec2 tc;
} vs_out;
void main(void) {
// Calculate the position of each vertex
vec4 pos_vs = mv_matrix * position;
// Pass the texture coordinate through unmodified
vs_out.tc = tc;
gl_Position = proj_matrix * pos_vs;
}}
Fragment shader with a single texture
coordinate
#version 450 core
layout (binding = 0) uniform sampler2D tex_object;
// Input from vertex shader
in VS_OUT {
vec2 tc;
} fs_in;
// Output to framebuffer
out vec4 color;
void main(void) {
// Simply read from the texture at the (scaled) coordinates and
// assign the result to the shader's output.
color = texture(tex_object, fs_in.tc * vec2(3.0, 1.0));
}