Introduction to OpenGL Shaders: OpenGL, GLSL, Shaders, and Fragments

2020-08-19 15:18:00 | #programming #glsl #opengl #webgl | Part 1 of 4

Tested On

  • Linux Ubuntu 20.04
  • Windows 10
  • macOS Catalina

In this tutorial series, our aim is to gain a deep understanding of the fundamentals of OpenGL, for the purposes of developing simulations and games.

With that focus in mind, we're going to fast forward through some of the history of OpenGL, and write a simple shader as soon as possible, so that we can begin to deconstruct and understand how it works.

What is OpenGL?

OpenGL is a software API that allows developers to communicate with graphics hardware (GPU) to render 2D and 3d graphics, by invoking functions and constants. OpenGL is managed by the non-profit technology consortium Khronos Group, who continuously update specifications to reflect new features and capabilities.

What is GLSL (OpenGL Shader Language)?

GLSL gives programmers the ability to write shaders, more easily, using syntax similar to the C programming language. With GLSL, we can avoid verbose, low level, hardware-specific assembly language as long as hardware manufacturers conform to the OpenGL spec.

What is a Shader?

In this introductory series, we're primarily focused on fragment/pixel shaders. For now, think of a shader as a computer program that shades individual pixels to present the illusion of depth, light, and color. Think of how a sphere is shaded, with the light areas made of bright pixels and shadowy areas made of dark pixels. The shader had to perform millions of calculations, at the end of which, has to instruct each pixel to be a certain color, brightness and transparency.

In the future, we will also cover vertex, geometry, and compute shaders.

What is a Fragment?

A fragment is the 3-dimensional x, y, z data that represents what is to be rendered. A pixel is the 2-dimensional, grid of RGB (Red, Green, Blue) dots that comprise the display's screen. 3D fragment data is processed into 2D pixel color data for every pixel on screen, to produce a 2D image that has the illusion of 3D form and depth. In the end, pixels are still just combinations of RGB values.

So in a screen that is 800 pixels wide by 600 pixels tall, there will be at least 480,000 fragments, each with color, transparency, depth, stencil, and raster information. At 60 frames per second, that's 28,800,000 fragments that have to be shaded, per second. Your shader code defines data for each of those millions of fragments, which is rendered as RGB pixels, resulting in the images on your screen.

How to Run GLSL Fragment Files

The easiest way to code and run .frag files, with no setup, is through the online GLSL Editor from Patricio Gonzalez Vivo.

If you're looking for a more native experience to Linux, follow these installation instructions to install glslViewer. This is an OpenGL sandbox that will render our shader with a simple command: glslViewer path/to/file.frag. Code your .frag file using any editor you like, and run it with the glslViewer.

For Windows 10 and macOS users, Visual Studio Code provides easy to install extensions for GLSL syntax highlighting and rendering. Hit F1, type glsl, and click "Show glslCanvas" to display the renderer alongside your code.

For a cross-platform solution, there's also the open source Shader maker by Rene Weller.

How to Write Your First OpenGL Shader

Let's start with some basic shader code that displays a color to the screen, and explain line-by-line.

Filename: color.frag

#ifdef GL_ES
precision lowp float;
#endif

void main() {
    vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
    gl_FragColor = color;
}

Line 1: #ifdef is one of many preprocessor directives that we use to define conditions for certain compilation blocks and values. In this example, we are checking to see if GL_ES(GL Embedded Systems) is defined before we set the precision of the float data type.

Line 2: Here, we set the precision level of floating point (decimal) numbers to lowp. If we set the value to highp, the GPU would store more decimal places for each number, making our calculations more precise at the cost of performance. A good rule of thumb is to use highp when dealing with vertex positions, mediump with texture coordinates, and lowp when working with just colors.

Line 3: Just defines the end of our if block of code.

Line 5-7: Every fragment shader requires a void main function to define what gets executed when you run the shader. main is the name of the function and the code inside will execute for each fragment.void just means it doesn't return anything. Normally, functions perform some calculation and return a value - like the sum of 2 numbers, for example. But main has nothing it needs to return. It just executes.

Line 6: Here, we create a vec4 variable named color. vec4 is just a 4-dimensional floating-point vector. In simple terms, it's data in the form of 4 decimal values. We need 4 decimals to represent an RGBA color that is comprised of Red, Green, Blue, and Alpha (transparency). If you set all values to 0.0, you would get a black, transparent color. Setting them all to 1.0 produces a white, opaque color. In this example, we are setting Red and alpha to the maximum value of 1.0, but feel free to experiment with different combinations in your code.

If we were to store X and Y coordinates, only a vec2 would be necessary. For RGB, without alpha data, vec3 is sufficient. But since we need 4 values, we store them in a vec4.

Line 7: gl_FragColor is one of many built-in variables of GLSL. Built-in variables allow us to read and write values to the OpenGL pipeline, affecting what gets rendered by each fragment/pixel. By assigning one color to gl_FragColor, OpenGL will render that color across every single fragment.

Render this shader on Linux with the glslViewer color.frag command. Or within VisualStudio on Windows 10 and macOS.

You should see that all of the pixels have been shaded a solid red color, like so:

Experiment with different colors:

RGBA Values Output
(0.0, 1.0, 0.0, 1.0)
(0.0, 1.0, 1.0, 1.0)
(1.0, 0.5, 1.0, 1.0)

Understanding Fragment Shading

Now let's dive deeper into what is happening to each pixel as the fragment shader renders our solid red image. We're going to learn about another built-in variable called gl_FragCoord. This returns the x, y, z, and 1/Wc coordinates of each individual fragment. X is the fragment's position along the x-axis (left to right), y is top to bottom, z is it's depth on a 3D plane, and 1/Wc is the clip-space (a clipped range) vertex position. You don't need to be concerned with z or 1/Wc for this tutorial. We're dealing with a 2D plane, so just x, and y are important.

So to recap that last paragraph in plain English, we have a gl_FragCoord that we can read the x and y position of each fragment from. Let's update our code to better understand this concept. Before running the shader, we'll explain each new line of code, in detail.

#ifdef GL_ES
precision lowp float;
#endif

uniform vec2 u_resolution;

void main() {
    vec2 coord = gl_FragCoord.xy / u_resolution;
    vec4 color = vec4(coord.x, 0.0, 0.0, 1.0);
    gl_FragColor = color;
}

Line 8: Here, we are directly referencing gl_FragCoord.xy. Before we explain why we are dividing its value by u_resolution, let's make sure we understand the xy coordinates that are being returned. Remember, main gets executed for each fragment. We don't need to be concerned with the order that each fragment is being rendered. Whether top to bottom, left to right, or in parallel - it doesn't matter. But everytime main executes on a fragment, we can access that fragment's x, y, and z coordinate. Here's an illustration that demonstrates how the screen space xy coordinates of a fragment translate to pixels.

From the OpenGL specifications: "By default, gl_FragCoord assumes a lower-left origin for window coordinates and assumes pixel centers are located at half-pixel coordinates. For example, the (x, y) location (0.5, 0.5) is returned for the lower-left-most pixel in a window..."

So on an 800x600 monitor, you'll get pixel x coordinates in the range of 0.5 - 799.5. Since we can't pass numbers greater than 1.0 into our vec4 color (0.0 - 1.0 only), we need to normalize the values to fall inside that 0.0 - 1.0 range. We do this by dividing the x and y values by the resolution of the screen. We perform this normalization on line 8, where we take the vec2 gl_FragCoord.xy and divide it by the vec2 u_resolution (800.0, 600.0). When OpenGL processes the bottom left fragment, the xy equals (0.5, 0.5). We divide the fragment vec2(0.5, 0.5) by the resolution vec2(800.0, 600.0), which gives us a vec2(0.0, 0.0) that we store into the vec2 coord. We then pass the x coordinate coord.x into the vec4, which produces the color vec4(0.0, 0.0, 0.0, 1.0), a solid black pixel.

To better illustrate how the gl_FragCoord values change every time the main loop runs on each fragment, take a look at the following pseudocode, which describes the calculations of a few pixels.

// Bottom left pixel
gl_FragCoord.xy = (0.0, 0.0)
u_resolution = (800, 600)
coord.xy = (0.0, 0.0) / (800, 600) = (0.0, 0.0)
color = vec4(0.0, 0.0, 0.0, 1.0) // solid black

// Center pixel
gl_FragCoord.xy = (399.5, 299.5)
u_resolution = (800, 600)
coord.xy = (399.5, 299.5) / (800, 600) = (0.499375, 0.49916667)
color = vec4(0.499375, 0.0, 0.0, 1.0) // red with half intensity

// Last pixel
gl_FragCoord.xy = (799.5, 599.5)
u_resolution = (800, 600)
coord.xy = (799.5, 599.5) / (800, 600) = (0.999375, 0.99916667)
color = vec4(0.999375, 0.0, 0.0, 1.0) // solid red

So after this program runs, we end up with a gradient going from pitch black on the left, to solid red on the right. Try running the program to see the gradient for yourself. Also, try passing coord.y into vec4 and notice how the direction of the gradient changes from left to right to bottom to top.

RGBA Values Output
(coord.x, 0.0, 0.0, 1.0)
(coord.y, 0.0, 0.0, 1.0)
(coord.x, 0.0, 0.0, coord.y)

Line 5: Before we wrap up, let's explain this line of code. uniform designates this variable as read-only, and equal across all threads (uniform) during shader invocation. vec2 just means it stores 2 vector values. In this case, the width and height of the screen. u_resolution is a glslViewer/glslCanvas built-in variable, just like gl_FragCoord. All you have to do is declare u_resolution , and you will have access to the screen resolution. If you aren't using glslViewer, but ShaderToy, you will have to reference resolution with iResolution, instead.

How to Print GLSL Values to the Screen for Debugging

Unfortunately, there is no easy way to print values to the screen like you would with Python's print or JavaScript's console.log. Reason being, main runs on every single fragment so you would be flooded with millions of text values. Debugging is not the focus of this tutorial, but I will try to explain the debugging process anyway, to give you a better sense of how to handle values.

While debugging, you can normalize the values so that they fall inside the 0.0 - 1.0 range and display it as a color. For example, if you expect a variable's value to equal 500.0, somewhere in your code, divide it by 500.0, which normalizes the value to 1.0, then pass it into the R position of your color vec4(R, 0.0, 0.0, 1.0). If you get a solid red color, the value is at least 500.0, like you expect. Otherwise, you can use the color variation to see how far off you are.

OpenGL & GLSL Programming Exercises

Try to solve the following problems, using everything you've learned up to this point. Feel free to share solutions in the comments. Optimize each solution, as much as possible.

  1. Write an OpenGL fragment shader that produces a solid yellow background

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    void main() {
        vec4 color = vec4(1.0, 1.0, 0.0, 1.0);
        gl_FragColor = color;
    }
  2. Write an OpenGL fragment shader that produces a solid white background

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    void main() {
        vec4 color = vec4(1.0, 1.0, 1.0, 1.0);
        gl_FragColor = color;
    }
  3. Create an OpenGL fragment shader that produces a linear gradient that goes from solid white at the top edge to solid black at the bottom

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    uniform vec2 u_resolution;
    
    void main() {
        vec2 coord = gl_FragCoord.xy / u_resolution;
        vec4 color = vec4(coord.y, coord.y, coord.y, 1.0);
        gl_FragColor = color;
    }
  4. Write an OpenGL fragment shader that produces a linear gradient that goes from solid blue on the left edge to solid black on the right

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    uniform vec2 u_resolution;
    
    void main() {
        vec2 coord = gl_FragCoord.xy / u_resolution;
        float gradient = 1.0 - coord.x;
        vec4 color = vec4(gradient, gradient, 0.0, 1.0);
        gl_FragColor = color;
    }
  5. Create an OpenGL fragment shader that produces a linear gradient that goes from solid red on the left edge to solid blue on the right

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    uniform vec2 u_resolution;
    
    void main() {
        vec2 coord = gl_FragCoord.xy / u_resolution;
        vec4 color = vec4(1.0 - coord.x, 0.0, coord.x, 1.0);
        gl_FragColor = color;
    }

Want To See More Exercises?

View Exercises

Comments

You must log in to comment. Don't have an account? Sign up for free.

Subscribe to comments for this post

Want To Receive More Free Content?

Would you like to receive free resources, tailored to help you reach your IT goals? Get started now, by leaving your email address below. We promise not to spam. You can also sign up for a free account and follow us on and engage with the community. You may opt out at any time.



Tell Us About Your Project









Contact Us

Do you have a specific IT problem that needs solving or just have a general IT question? Use the contact form to get in touch with us and an IT professional will be with you, momentarily.

Hire Us

We offer web development, enterprise software development, QA & testing, google analytics, domains and hosting, databases, security, IT consulting, and other IT-related services.

Free IT Tutorials

Head over to our tutorials section to learn all about working with various IT solutions.

Contact