Kicks Condor

The Key To Writing Shaders… Varies

The thing—the key—the realization that’s needed before you can write shaders. Really, I think this will help.

Writing shaders has clicked for me today. By shaders, I mean: 3-D shaders, GLSL shaders, OpenGL shaders. They are little programs that draw things. With math. Right on the graphics card.

Being a newb, I can see what tripped me up. The tough thing about shaders is that they aren’t like other programs. They don’t just run once through and there’s your drawing.

Take this circle:

Well, here’s the shader that did that:

varying vec2 v_pos;
void main() {
  float t = step(1.0, length(v_pos));
  gl_FragColor = vec4(0.3, 0.4, 0.7, 1.0 - t);
}

How could THAT POSSIBLY draw a cirCLE?!


The Little Blue Fragments

Let’s start with something easy: why is it blue?

gl_FragColor = vec4(0.3, 0.4, 0.7, 1.0 - t);

This line sets the color of a single pixel. Or—as the shader calls it—a single fragment.

In shaders, colors are represented by a series of four numbers—0.0 to 1.0 for red, green, blue and alpha. The vector of (0.3, 0.4, 0.7) is this blue color. In hex colors, we’re looking at #4C66B2.

Our circle starts its life as a square. I’ve already setup a 200 pixel by 200 pixel canvas. The shader will be painting this entire area.

Oh hey, I know: let’s just set the alpha to 1.0. This means the blue will not be transparent at all—it will be solid.

gl_FragColor = vec4(0.3, 0.4, 0.7, 1.0);

That paints our whole square:

So I am specifically talking about fragment shaders here.

In this fragment shader, we’re now saying, “Paint this pixel solid blue.” And it’s just doing that for every pixel in the square.

200 x 200 = 40,000.

This shader runs—not once—but 40,000 times!


Now Let’s Zoom Out

I’ve setup a canvas that goes from (-1, -1) to (1, 1)—with (0, 0) in the middle—and I’ve told the fragment shader to paint this area.

Our drawing area.

Here we’ve set up four vertices—those are the points on the corners—going from -1 to 1 on each side. We’ll set up OpenGL to pass in these four coordinates—and the vertex shader will pass these on to our fragment shader. And we’re drawing the circle inside that box.

This all seems pretty easy, right? 🤷


Explain This One

Ok, this time, we’re going to go with a dead simple fragment shader:

varying vec4 v_color;
void main() {
  gl_FragColor = v_color;
}

We’re going to pass in the color through a variable.

In order to do this, we need to write a separate vertex shader:

varying vec4 v_color;
void main()
{
  gl_Position = vec4(position, 1.0);
  v_color = vec4(0.3, 0.4, 0.7, 1.0);
}

Not too bad, yeah? We have a variable v_color we’re using to pass the color. And we’re passing the color blue.

The gl_Position variable is passing a corner over—such as (-1, -1) or (1, -1). These were set up in my OpenGL code.

Our vertex shader runs once for every corner; our fragment shader runs for every pixel. Together, they draw the square:

Okie—now let’s make a subtle change. Let’s make it so one corner is blue. The (1, 1) upper right corner will be blue. And the rest will be green.

Our vertex shader could be written like this:

varying vec4 v_color;
void main()
{
  gl_Position = vec4(position,1.0);
  if (position.xy == vec2(1.0, 1.0)) {
    v_color = vec4(0.3, 0.4, 0.7, 1.0);
  } else {
    v_color = vec4(0.3, 0.7, 0.4, 1.0);
  }
}

Ok, good—if the coordinate is (1, 1) then the color will be blue.

And—with that change—we get this:

That makes sense—the upper corner is certainly blue. But how in the world did we get a gradient??

Look at that vector shader again. Shouldn’t just the (1, 1) coordinate be blue? Why is the (0.99, 0.99) coordinate also blue??


Like I Said: It Varies

How do we pass these coordinates to the fragment shader? We use varying.

The key to all of this is varying.

🙌   PLEASE NOTE: More modern OpenGL fragment shaders (such as those in OpenGL 3) use in and out rather than varying. They STILL vary, though!

Now, normally when you pass stuff around in a program, you use a variable.

You might say a = 1. Ok, so now a is one. And it’ll always be one. Until you change it. So a = 911. Now a is nine-eleven. Nice, you turned an innocent variable into a crisis. And it will stay that way.

But WOAH WOAH WOAH—that is NOT what varying is. When you pass something through varying it is not just a variable. It is now a range.

😳   A range??

A crazy, slidy, slippery range.

😳

I’m sorry, it’s true.

🙄   Don’t apologize, I’m sure there’s a reasonable explanation.

Oh, you’re right—I forgot—it’s awesome!! I apologize for apologizing.

Remember how the vertex shader ran once for each corner of the box? Here it is again, so you can look at it.

varying vec4 v_color;
void main()
{
  gl_Position = vec4(position,1.0);
  if (position.xy == vec2(1.0, 1.0)) {
    v_color = vec4(0.3, 0.4, 0.7, 1.0);
  } else {
    v_color = vec4(0.3, 0.7, 0.4, 1.0);
  }
}

The vertex shader DOES color just the corners. Like this:

How the vertex shader sees the colors.

And then when those colors are passed through the varying variable—mind you, an ACTUAL varying variable!—the fragment shader sees a range from green to blue along the top x-axis and the right y-axis.

How the fragment shader sees the colors.

And because of varying, our little v_color is going to be different for every pixel. It’s going to be a color—a vec4, that is—somewhere between green and blue.

It’s going to go smoothly from green to blue, without your interference.

Why did it paint a solid blue earlier? Because all the corners were same. So—even though we used varying—all corners were the same, so all values in between were the same.


Circle Time

Back to our circle. The trick here is: we are going to use—not the varying color—but the varying position to determine if we are inside or outside the circle.

varying vec2 v_pos;
varying vec4 v_color;
void main() {
  float r = length(v_pos);
  v_color.a = 1.0 - step(1.0, r);
  gl_FragColor = v_color;
}

You already understand the color. The vertex shader has given it to us.

What we turn our focus to now are these two built-in functions: step and length.

🙌   SPECIAL NOTE: You can browse a complete list of built-in functions at Shaderific.

The length function gives us the distance of our position from the origin. The origin is the center of our circle. So, in this case, that’s our radius.

float r = length(v_pos);

Now, remember, we’re dealing with a varying variable here, so r is the distance of the current pixel we’re painting from the center.

The length function is also going to get rid of negative values here. The point (-1, -1) has a distance of 1.414, just like the point (1, 1) does.


The step function is going to take everything above 1.0 and make it exactly 1.0. And everything below 1.0 will be 0.0.

So, after step, the value will be 0.0 if we’re inside the circle. And 1.0 if we’re outside it.

See how the shader looks without the step function.

Now—if you want to play with these shaders (in a WebGL-compatible browser), you can do so here. Just go to the upper-right corner and switch to Cube. Which will give you a 2-D plane.

(You can also View Source on this page—this page contains all of its shaders and renders them inline with the text.)


A Few Things You May Still Wonder About

So, even though shaders look a lot like JavaScript or C, they actually have a few syntax differences that make the above codes possible.

Like take this line. How does it make the comparison here?

if (position.xy == vec2(1.0, 1.0)) {

Vector math is built-in with shaders. So you can ask if two vectors are equal using the double-equals.

You can also add, multiply, subtract vectors and so on—just by using the operators.

However, I am comparing a vec4 position with the vec2. And I am using “swizzling”.

position.xy   /* a vec2 */
position.xyz  /* a vec3 */
position.a    /* a float */

This special syntax lets me quickly grab the x, y, z or a parts of a vector.

Composing vectors also has a simplified syntax. Do you remember these lines from our final circle shader?

v_color.a = 1.0 - step(1.0, r);
gl_FragColor = v_color;

This could be simplified. You can use the vec4 function to compose a vector from multiple vectors.

gl_FragColor = vec4(v_color.xyz, 1.0 - step(1.0, r));

Here I am passing in a vec3 and a float—which will be assembled, in order, to form a new vec4. Similarly, I could pass in two vec2 vectors.

As long as your arguments have four parts in total, you can build a vec4 from them. Of course, this is also true of vec2, vec3 and any other type in the GL shader language.

This post accepts webmentions. Do you have the URL to your post?

You may also leave an anonymous comment. All comments are moderated.

PLUNDER THE ARCHIVES

This page is also at kickssy42x7...onion and on hyper:// and ipns://.