Check out a free preview of the full Advanced Creative Coding with WebGL & Shaders course

The "Fragment Shaders" Lesson is part of the full, Advanced Creative Coding with WebGL & Shaders course featured in this preview video. Here's what you'd learn in this lesson:

Matt explains how fragment shaders strictly use math and numbers to create imagery.


Transcript from the "Fragment Shaders" Lesson

>> In order to sort of walk through shaders, I'm gonna go through the guide book again. There'll be some interactive demos and we'll try and break it down there. So if you want, you can open up the repo and follow along. It'll just be in the guide book in the section on shaders.

You can click the menu item and just go straight to shaders. So this here is a shader. This is an example of a fragment shader. It's an example of a full screen fragment shader which covers the entire canvas that we're drawing this little box here. And so this is the way that many people understand shaders.

But we're gonna see how we can apply shaders not just to a square like this or a rectangle. But we can actually apply them to 3D meshes. We can manipulate a sphere and manipulate the surface of the sphere with a fragment shader. Fragment shaders, they typically always start with this syntax precision highp float.

This syntax here is saying, for all my floating point numbers, I want them to be very high precision. I want them to be highly precise floating point numbers. It's just a standard thing that you copy and paste into every single fragment shader. And then we have some definition of uniforms.

And so I'll talk about uniforms in a second, but for now you can just look at these types, vec2, float. There's other things like there would be something like int. int would be an integer like 1, 2, 3, 4. And so you're starting to see that the syntax of GLSL is very highly tight.

And so we have to whenever we're declaring a variable, we have to declare the type that variable is, whether it's a floar, or an int, or a bool, or vec2, vec3, vec4. Vec2, vec3, vec4 is a bit like vector 2, vector 3, vector 4 which we've been doing this morning with Three.js.

It's a little similar, it's something that holds x, y, z components. And so then every shader also has a void main. So it has this function called main and the function doesn't return anything. And in GLSL, when you have a function that doesn't return anything, you say void.

If the function returned to float, you have to say that the function returns a float. And you'll actually notice if you write this, it's gonna give you an error because it's now saying the main function can't return value. So it's gonna give you errors pretty quickly. It's not as forgiving as JavaScript.

It has to be very exact. If you forget a semicolon, it's gonna give you an error. Another annoying thing is if you don't put a decimal inside of the number sometimes. Right here it's working but sometimes you'll be working locally and you'll be working in a different computer.

And sometimes just forgetting a decimal and a floating point value will give you an error and that's kind of annoying as well. Because if I took the decimal place out of, let's say, this 2.0 here. Instead of writing 2.0 I wrote 2, in some compilers, depending on the computer, depending on the GPU and other things, this might get considered as an integer instead of as a float.

So that's just something to be mindful of is that we're always gonna do decimal values for floats. And so inside of our main function, we have some other lines. We have this thing vec2 uv. So that's like declaring, it's like we said like there var uv is equal to, or const uv is equal to.

We've just declared the actual type that's associated with this variable uv. And then another thing you might notice is that we're able to do operations like multiplication, division, addition. We're able to do that across x, y, z all together. So here we're saying take this variable, which is some thing called gl_FragCoord, which I'll explain a little bit later, maybe.

But we're taking this variable which has xy, and we're dividing it by a resolution, which is a vec2 defined up here at the top. And the resolution also has an xy. And so something you'll notice is that it's not just saying okay, I'm gonna do x divided by x, and then y divided by y, and then z divided by y.

But you can actually just do the components. So xy divided by the other thing xy. So it's basically just a syntax thing. You can start to do multiplication, and division, and operations on all components within a vector, as opposed to doing them one at a time. So yeah, this is just like I was saying, it's just like an example of a fragment shader.

It's sometimes called a pixel shader. So fragment shader and pixel shader are kind of interchangeable for most of our purposes. Let's just consider them the same thing. And the reason it's called a pixel shader is because this type of shader deals with pixels. And so here the whole purpose of this piece of code, the entire purpose is just to say, what is the color of each of the pixels within this rectangle?

And so here I've got a rectangle or a quad, you might call it an OpenGL or WebGL land. And I'm just measuring this, it's 530 pixels by around 500 pixels. So it's 530 pixels wide by around 500 pixels high. And so that means we have 530 * 500.

That means this square is drawing 265,000, well, 265k pixels in this square and remember, this is being done 60 times a second. So it's doing 265,000 pixels 60 times a second. And not only that, but because I'm on a retina display, I'm on a Mac, it's actually a pixel density of 2.

And so it's multiplied that by 2. That's how many pixels it's drawing every single frame in the computer. And it's a lot of pixels. It's like really, really fast. It's crazy how fast these programs are running because it's drawing these every second, 31 million times or so. And so the reason it's so fast and this is sort of something.

This is the the big thing that makes writing shaders different than writing any other type of code. But the reason it's so fast is that it's parallel. And so the way it works is we say, here's the shader code, apply it to every single pixel on the square.

And apply it to every single pixel in this square exactly in parallel time. So each of these pixels is being drawn at the exact same moment, every frame. And so it's not going by one by one pixel. It's not going from the top left and drawing each individual pixel one by one.

But instead, the GPU is saying, okay, I have this code. I'm gonna run this code across every single pixel all at once on this square, and immediately the image will be drawn. And so because it's parallel, it means we can't access neighbouring pixels. So this pixel down here, somewhere in here, we can't say, well, I wanna make this pixel blue if this pixel up here is yellow.

I can't make those kind of conditional statements like I can with JavaScript, because this pixel here is being drawn at the exact same time as this pixel here. And so they don't know which one is, they don't know where their neighbors are. They don't know anything about what the pixels around them are gonna look like, or what the pixels up in the top left corner are gonna look like.

So this is just to say because it's parallel, we have to sort of shift our mindset. And it's a huge paradigm shift. We're switching from typical JavaScript rendering to thinking just in terms of single individual pixels, or in the case of vertices. We're gonna talk about single vertices.

Okay, so that's me trying to explain the basics of the fragment shader. And again, here's some of the syntax. This little cheat sheet of syntax also exists in the repo, in the guides folder. So if you're wanting a bit of a refresher, you can just pop that open.

Like I was saying, there's float, there's vec, there's int, there's bool. There's things that are called sampler2D, which hold the texture. So when we go to do texturing, we would use this. And then there's matrix data types, which has which has to do with transformations. If we're transforming a vector by some rotation or something like that, we might use this data type called mat4 or four by four matrix.

We don't need to get too far into matrices today, though. Here is just some example and here are some more examples. And, yeah, the thing to sort of get our heads around is that we can create an rgb vec just like we were doing with Three.js. But if we just pass a single component, it's gonna use that for x, y, z.

And we can also sort of compose, let's say, a vec 4, we can compose that by giving it a vec3 and a single float. And so the rgb would be a vec3 with rgb, and then the float would be alpha, which is just playing 5. So we can compose like a vec4 by using a vec3 and a float here.

And to access the values, in Three.js we were just having to do one by one, without having to say position.x, position.y. But we can access them all at the same time. We can access .xy which is gonna give us back a vec2. We can access them using the rgba syntax instead, which is useful for colors, and we can just access one thing like that.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now