Introduction to OpenGL Functions: Distance, Length, and Step

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

Tested On

  • Linux Ubuntu 20.04
  • Windows 10
  • macOS Catalina

In this tutorial, we're going to start taking advantage of OpenGL's built in functions. A function is just code that is wrapped around a name we can invoke, as needed. So for example, it might take 10 lines of code to calculate the distance between two points. Instead of writing those 10 lines every time, we can instead put that code into a function called distance and just call distance(vec2(x1,y1), vec2(x2,y2)) in one line of code.

Luckily, we don't need to create our own distance function. OpenGL has its own built in function for that. We don't need to be concerned so much about how a function does something, but we do need to know how to use it and the output that gets returned.

Calculating the Distance Between Two Points with the OpenGL Distance function

You may remember the Pythagorean theorem from math class, which allows us to determine the length of c (the hypotenuse), as long as we know the length of a and b of a right triangle. The Pythagorean theorem states that a2 + b2 = c2 or :

Since we're dealing with points, the diagram above is a better representation of the distance (d) between two vec2 fragments. Remember, each pixel on your computer screen is positioned somewhere along the width (x) and the height (y), with 0,0 being the bottom left of your screen, and 800,600 being the top right of an 800px by 600px sized monitor. So the fragment that draws the top right pixel will have a vec2 position of (800,600), with x = 800, and y = 600.

So if we want to know the distance between a pixel at (0,20) and a pixel at (24, 58), we just plug it into the formula. Knowing x1,y1 represents the first pixel at (0, 20), and x2,y2 represents the second pixel at (24,58), we can calculate the distance between them like so:

(x2 - x1)2 + (y2 - y1)2 = d2
(24 - 0)2 + (58 - 20)2 = d2
576 + 1444 = d2
square_root(2020) = d
44.944 = d

The built-in distance function does just that by calculating and returning the distance between two points in screen space. Let's write some shader code to better understand this concept.

Filename: radial.frag

#ifdef GL_ES
precision lowp float;
#endif

uniform vec2 u_resolution;

void main() {
    vec2 position = gl_FragCoord.xy / u_resolution;
    vec2 offset = vec2(0.0, 0.0);
    float dist = distance(position, offset);
    vec3 color = vec3(dist, 0.0, 0.0);

    gl_FragColor = vec4(color, 1.0);
}

Remember, the main function runs on every single fragment/pixel, so it's going to calculate a ton of distances.

Line 8: First, we divide the vec2 gl_FragCoord.xy with the vec2 u_resolution, which returns a normalized fragment position that falls within (0.0-1.0, 0.0-1.0). If you're not familiar with normalizing, please review the last chapter.

Line 9: Then, we establish a point at (0.0, 0.0) named offset. We're going to treat this as a normalized point, meaning we will only give it values between 0.0 and 1.0.

Line 10: When the main function runs against the bottom left fragment with a position of (0.0,0.0), we pass it into the distance function, along with origin, which returns the distance between them. In this case, it will return 0.0, which we store into the dist variable.

Line 11: We then plug that 0.0 dist value into the RED value of our vec3 color, which will produce a solid black pixel for the current fragment at the (0.0,0.0) position.

Let's run this shader on Linux with the glslViewer color.frag command. Or within VisualStudio on Windows 10 and macOS.

You should see the following radial gradient. But why?

Let's walk through some more scenarios to fully grasp why this image gets produced. We already know what happens when a fragment at (0.0,0.0) is shaded. What about a fragment at the pixel position (56.0,75.0)?

  1. Line 8: vec2(56.0,75.0) gl_FragCoord.xy gets normalized to vec2(0.07,0.125) after being divided by the vec2(800,600) u_resolution and is assigned to the vec2 position variable
  2. Line 10: We pass the vec2 position and vec2 origin into distance to calculate the distance between (0.0, 0.0) and (0.07, 0.125), which is 0.14326548... and assign that float value to the distance variable.
  3. Line 11: We pass distance into the RED value of our vec3 color, which produces a slightly redder hue than the (0.0,0.0) pixel.
  4. Line 13: We pass color, along with a 1.0 alpha value into a vec4 that we assign to gl_FragColor, which tells the GPU to shade the pixel with our red color.

Now if you were to perform the calculations yourself, pixel-by-pixel, you'd come out with RGBA values that would produce the same red radial gradient.

Calculating the Length of a Vector with the OpenGL Length function

The length function performs the same calculation as distance, but rather than returning the distance betwee two points, it returns the distance between the coordinates of a vector. In other words, it returns the length of a vector. Take a look at the following example:

Filename: radial.frag

#ifdef GL_ES
precision lowp float;
#endif

uniform vec2 u_resolution;

void main() {
    vec2 position = gl_FragCoord.xy / u_resolution;
    float len = length(position);
    vec3 color = vec3(len, 0.0, 0.0);

    gl_FragColor = vec4(color, 1.0);
}

Notice that length accepts only one vec2, instead of two. Like our previous shader, this will calculate how red each pixel is based on its fragment's coordinates. So pixels closer to the top and right edges will produce redder hues. We can do a similar calculation exercise that we did with the distance function. Let's run the fragment at position (180.0,30.0) through this logic:

  1. Line 8: vec2(180.0,30.0) gl_FragCoord.xy gets normalized to vec2(0.225,0.05) after being divided by the vec2(800,600) u_resolution and is assigned to the vec2 position variable
  2. Line 10: We pass the vec2 position length to calculate the distance between (0.225, 0.05), which is 0.23 and assign that float value to the len variable.
  3. Line 11: We pass len into the RED value of our vec3 color, which produces a redder hue than the (0.0,0.0) pixel.
  4. Line 13: We pass color, along with a 1.0 alpha value into a vec4 that we assign to gl_FragColor, which tells the GPU to shade the pixel with our red color.

The OpenGL Step Function

The step(edge, x) function compares two values, and returns 0.0 if x[i] < edge[i]. Otherwise, it returns 1.0. This basically, pushes values to the most extreme ends, with no range in between. Take a look at the following example:

Filename: step.frag

#ifdef GL_ES
precision lowp float;
#endif

uniform vec2 u_resolution;

void main() {
    vec2 position = gl_FragCoord.xy / u_resolution;
    float x = step(0.5, position.x);
    vec3 color = vec3(x);

    gl_FragColor = vec4(color, 1.0);
}

Line 9: Here, we create a color based on the fragment's normalized x position. Remember, when we divide gl_fragCoord.xy by u_resolution we get coordinates that fall within 0.0 and 1.0. So a fragment with an x position of 200 becomes normalized to 0.25 on a screen 800px wide because 200 divided by 800 is 0.25. So for that pixel, when we pass its position.x into step along with the 0.5 edge, step returns 0.0 because 0.25 < 0.5. Knowing this, we can determine that every fragment with an x coordinate less than 0.5 will return 0.0. And every fragment 0.5 or above will return a 1.0.

Line 10: We can then pass that value into a vec3 color. We've been used to passing 3 values into color to represent RGB, but when RGB are all the same value, you can just pass in one and it wil make the correct assumption. So passing in a 0.0 will produce an RGB of 0,0,0 (solid black) and a 1.0 produces a solid white.

Now that you know how to shade pixels to produce gradients and contrasting colors, you're ready to begin creating shapes. Try experimenting on your own, before moving to the next chapter. These are 3 very simple, yet useful functions you'll be seeing more of so it's best to get an intimate understanding of them.

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 radial gradient that is red in the bottom left corner and black in the remaining edges

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    uniform vec2 u_resolution;
    
    void main() {
        vec2 position = gl_FragCoord.xy / u_resolution;
        vec2 offset = vec2(0.0, 0.0);
        float dist = distance(position, offset);
        vec3 color = vec3(1.0 - dist, 0.0, 0.0);
    
        gl_FragColor = vec4(color, 1.0);
    }
  2. Write an OpenGL fragment shader that produces radial a gradient that is black in the center and red on the edges

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    uniform vec2 u_resolution;
    
    void main() {
        vec2 position = gl_FragCoord.xy / u_resolution;
        vec2 offset = vec2(0.5, 0.5);
        float dist = distance(position, offset);
        vec3 color = vec3(dist, 0.0, 0.0);
    
        gl_FragColor = vec4(color, 1.0);
    }
  3. Create an OpenGL fragment shader that produces a radial gradient that is red in the center and black on the edges

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    uniform vec2 u_resolution;
    
    void main() {
        vec2 position = gl_FragCoord.xy / u_resolution;
        vec2 offset = vec2(0.5, 0.5);
        float dist = distance(position, offset);
        vec3 color = vec3(1.0 - dist, 0.0, 0.0);
    
        gl_FragColor = vec4(color, 1.0);
    }
  4. Create an OpenGL fragment shader that produces a 45 degree slope with the top left half shaded white and the bottom right half shaded black

    Expected Output:

    #ifdef GL_ES
    precision lowp float;
    #endif
    
    uniform vec2 u_resolution;
    
    void main() {
        vec2 position = gl_FragCoord.xy / u_resolution;
        vec3 color = vec3(step(position.x, position.y));
    
        gl_FragColor = vec4(color, 1.0);
    }
  5. Create an OpenGL fragment shader that produces a background that is blue on the left half and red on the right half

    Expected Output:

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

This tutorial is an excerpt from our Pixel Manipulation with OpenGL Fragment Shaders course. If you'd like to learn more about how to shade pixels to produce 2D and 3D effects, consider signing up today.

Want To See More Exercises?

View Exercises View Courses

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 &amp; 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