Intro to Shaders – JavaScript & p5.js Course for Beginners

#Graphics Programming #Shaders #WebGL #GLSL #p5.js #Fractals #GPU #Programming
💬 Chat with this Video
Ask anything about this video…
This beginner's course is designed to teach you the fundamentals of graphics programming with zero prior experience required. You'll start by understanding how the GPU renders pixels and progressively learn core concepts like vertex and fragment shaders, domain repetition, and shaping functions. By following along in p5 js, you will apply these techniques to build a stunning animated fractal heart effect entirely from scratch. Developed by Pat Verra, this course is the perfect starting point for creative developers eager to break into WebGL and master the math of motion. >> Hi everyone. Today is the day. We're going to dive into the world of shaders. So, if you're like me just a few months ago, you'd be like, "What is a shader and why should I care?" Well, let me tell you why. If you have seen these beautiful sketches and wonder, "How are they made?" Or if you have created a sketch like this and wonder, "How can I make it run a little bit faster?" Shaders might be your answer. This video is the very first part of the shaders minieries where I walk you through the fundamental concepts of shaders. And by the end of the series, you'll be able to create this piece from scratch. So, let's answer the big question. What is a shader? And why does it make things run so much faster? The speed difference comes down to how the CPU and the GPU handle data. The CPU, or the central processing unit, is the main brain of your computer, built to handle complex logic, one task at a time. Think of a CPU like a master artisan. incredible at complex work but slow if forced to build thousands basic tables one by one. The GPU on the other hand is a specialized processor built to handle massive grids of data simultaneously by running task in parallel. Think of the GPU as a mass scale factory floor. Thousands of simple workstations executing identical task at once. A shader is simply the code written to run on the GPU. It is the single blueprint distributed to every workstation on that factory floor at the exact same moment. The trade-off for this speed is total isolation. Pixels on the GPU cannot communicate with each other. Your code has no idea what the pixel next to it is doing. The only reason these millions of isolated pixels can form a cohesive image is because every single workstation executes the exact same mathematical blueprint across a shared coordinate space. For this series, we'll be setting up our sketches in JavaScript using p5 js, but the actual shader code will be written in a dedicated language called GLSL or OpenGL shading language. While different frameworks require different boiler plate to get a shader on the screen, the core mathematical logic you write inside GLSL is universally transferable. The newer version of p5 actually has a library called p5 strands that allows you to write your shader code in JavaScript and it translates it to the GPU behind the scenes. And you can check out this amazing tutorial by Juan Rodrigue Garcia to learn more. But for this minieries, I thought that we would look at how the logic works fundamentally so that you can pick up your shader code and drop it in any environment that you choose. Okay. So now that we have the split between the setup code and the shader code, how do they actually talk to each other? To make that happen, your setup code or in my case my p5 js code needs to do exactly three things. First, it tells the browser to use the graphics card 3D pipeline or set it to the WebGL mode. And then it loads the GLSL text files into memory and sends them over to the GPU. And lastly, it sends information over to the GPU on every single frame. This live data includes two things. Dynamic global variables like running timer or your live mouse coordinate and the actual geometry data or the shape that you want to have drawn. To kick off the visual pipeline, the CPU sends a list of points to the GPU. And this is where your shader code takes over and processes those points into distinct stages across two shader files. The vertex shaders positions our geometry in space and the fragment shader determines the final color of every single pixel. When working in 2D, the vertex shader is incredibly simple. The CPU only needs to send four points or vertices off the four corners of the canvas. Our vertex shader is then just a boiler plate that stretches a flat canvas to frame the screen. Because of that, we will focus most of our energy inside the fragment shader that does all the painting. All right. Now, let's zoom into the fragment shader. If your canvas is the size of, let's say, 800 pixels x 600 pixels, the fragment shader runs exactly 480,000 times per frame. on a standard 1080p display, that scale increases to over 2 million individual pixels. Because of this massive scale, how you approach writing a fragment shader is going to be very different from how you write a standard p5 js sketch. Let's say when you draw a circle on a standard p5 sketch, you would write a function like an ellipse or a circle and you would give it the x ycoordinate and its size. But in a fragment shader, you're writing your code based on a perspective of a single pixel. Like mentioned before, each pixel is isolated and has no idea what its neighbor is doing. Without a way to talk to its neighbors, it solves for its color in four quick steps. First, it looks up its exact XY location on the screen. Then, it imports external context from the CPU like time or your mouse coordinate. And then it tests its position against your code asking for example is my specific distance from the center less than 200 pixels and if so set the color to blue and lastly it sends the finalized color value directly out to the screen. All right so I think we have enough context to start coding. So let's go. First what we're going to do is change the p5 version to the newer version p5 js version two. Actually, I'm Why don't use the 2.2.2? And then we're going to set the canvas mode. Change the canvas mode to WebGL mode. All right. So, with WebJL, the center, the origin is now at the middle of the canvas. So, why don't we try to draw a rectangle? So if I do rectangle of 0 comma 0 and a size of width and height you can see that now it is off the screen. So what we want to do is that we want to actually change this to width divided by two negative width divid by two and negative height / two so that it's position in the middle of the canvas. Next, what we want to do is that how about we create our shader variable. Let's call it my shader. And then inside I set up, we're going to load the shader files with we have not yet written. So let's do my shader equals to load shader and then we'll put the shader files in here. But as you can see, I am not using the pre-load function that I usually do in my other tutorials. And that is because we're using p5 js version 2. And instead of loading files using preload, we're going to be using async and await structure that allows us to load files asynchronously. Okay. So let's put async here and a wait before the loading function. That's all you need to do. and two files that we're going to load. Actually, why don't we create those files first? So, let's come to this arrow here, click the plus sign, and then click create file. We're going to call it shader. So, we're going to be creating two shader files. The vertex shader and then the fragment shader. So, I'm going to call it, as you can see here, shader.vert add file. And one more is shader.frag. frag. Let's go back to sketch. So now inside the load shader function here, we can put in shader.vert and then comma shader dot frag. Okay. All right. Click run. No error. We're all good. But nothing happens either. That's okay. Now, inside draw here, first I'm actually going to delete the background. We don't need background here. Run. Okay. And what we're going to put in here is a function called shader. And this basically tell the file that we're going to not use the default shading presets that comes with the WebJL mode. Let's use our shader file to create this about to be amazing sketch. So I'm gonna pass in my shader here and click run. And now it gives us an error because I guess there's nothing inside these two files yet. So before we start writing the vertex shader and the fragment shaders, I thought that we would look at the syntax of GISL and also the structure so that you get a little bit familiar with before we start writing. GISL inherits its rigid syntax from C. Every single statement must end with a semicolon. It is also statically typed requiring strict type matching. Even a whole number must include a decimal point to be recognized as a float. You must explicitly tell the GPU how much memory to allocate for decimals depending on your performance needs. You have three options here. You might notice later that I only write this line in our fragment shader. And that is because the vertex shader automatically defaults to high P. Because shaders handle spatial math and color spaces, you will work constantly with vectors. Despite GLSL's strict nature, vectors are incredibly flexible. First, you get shorthand initializers. Writing this automatically fills all three channels with 1.0. Second, you can access data using naming conventions that match your intent. Use XYZW for positions or RGBA for colors. They're completely interchangeable. And lastly, swizzling. Swizzling lets you instantly rearrange duplicate or slice components in a single line. For example, calling.bgr swaps the blue and red channels to instantly flip your color palette. Or grabbing xy from a ve 3 drops the third channel, instantly flattening it into a 2D vec 2. Now, how do these files actually look? Every shader revolves around a single main function, which acts as your entry point. To actually output your visuals, your code must write data to the GPU's dedicated target variables. In the vertex shader, your target is gl_osition, a built-in ve for that tells the GPU where geometry exists in space. And in the fragment shader, your target is a custom variable, usually named frack color, which is a vecore that dictates the final pixel color. To route data through this isolated world, everything flows in a strict single direction downstream. Data moves through this pipeline in three ways. It enters a stage as an input, it exits a stage as an output, or it gets broadcast globally from the CPU to every single pixel at once as a uniform. If you're looking at the older shader tutorials online, the syntax might look a little bit different. Here is a quick reference table showing how the keywords change between the original WebGL1 and the current WebGL 2. to tell the browser that we want to use this updated syntax, you need to write this as the very first line of both your shader files. Why the number 300? Because while browsers call this WebGL2, under the hood, it runs on the mobile hardware standard OpenGL ES3.0. That 300ES tag simply points directly to that version 3.0 spec. So now that you have all the context, let's start with the vertex shader files, shall we? So shader.vert. All right. So in here, we're going to start by declaring that this is the newer WebGL mode. Let's do this pound sign version 300. And I'm actually going to do the same thing inside the fragment shaders too, so I don't forget. All right, let's go back. Next, it's going to have one main function. So, let's do void main. And then the next thing is that we're going to be receiving the coordinate positions, the vertices of the canvas size from JavaScript or from the CPU, right? So actually JavaScript even though the size here is it's the size of the width which is 400 and the height of 400 right but when it sends to the GPU actually p5 converts it to values between zero to one. So what we're going to do here inside shader.vert convert is that we're going to use the qualifier call in, right? And then we're going to set the type which is ve 3 and then set this built-in variable called a position. And inside here we're going to write glosition and then set it to we cannot set it directly to a position because GL position needs a ve 4. So we're going to do ve for a position and then provide 1.0 as the last argument. Let's try and run this. Okay, it gives us an error and I think it's because we loaded the fragment shader but we have not written anything on it. So let's go there. So shaderfragment and same thing it has one main function. Yeah, let's just run this and see if there's an error. Okay, I guess that was it. So now nothing is being colored in because that is the job of the fragment shader. So let's do that. So let's go back to shader.vert. So now that we have in, we want to send out. So we want to send the position of these four coordinates to the fragment shader. So we're going to do out, but we're actually going to just send two points X and Y. Why don't we call it V pose? So that it's a position that is sending from vertex and we're going to send V post equals to a position.xy and this is what I described earlier as Switzling, right? So we're only taking the first and the second components of this ve 3, right? And then inside shader.frack, frack we're going to be receiving in VOST which is a vect 2 V pose we also need to declare frack color right in this new version of webgl and we're going to set frag color to it also is a vec 4 and the range is between zero and one 0.0 and 1.0 zero. So what we're going to do is how about actually let's set it to the color black and the fourth argument is the transparency. Right? So if I set it to 1.0 zero. Ah, frack color is a ve four. Nope. >> A few moments later. >> Ah, so I forgot to set the precision right in the fragment shader. So let's do that. So let's do precision medium P for the type float run. Yay. Okay. So we have the color black. Let's say if I change the first value here to 1.0, what do you expect? Red. That's correct. If I do both of these, you get magenta. That's great. But why is it in the top right corner here? That is because what is being sent from the CPU or P5 sketch is the values between 0 and one. So what I said earlier that the CPU sends this data here to the vertex shader is actually converts it to the value. It's mapped to a value between 0 and one. But the vertex shaders actually needs a clipping space that ranges between -1 and 1. So right now a position is between 0 and one. So as you can see here. So what we want is that we want to make sure that it is between -1 and one. What we can do is why don't we create a separate variable. So let's do a a ve 4 call position and set it initially as this. And then we're going to replace this here. So nothing is different yet, right? Because we just h what did I do wrong? Okay, I forgot that I'm not writing JavaScript anymore. This is JSL and I forgot a semicolon here. Works now. Okay. So what we need is we need to remap this, right? So to change from 0 to 1 to1 to one, what we need to do is I'm going to set the XY position to be equals to the current XY position times 2.0. Right? So between 0 and 1, we multiply it by two. So now becomes between 0 and two. And then we just subtract it by 1.0. zero. So now it becomes between -1 and one. And click run. Tada. Okay. Now let's go back to the fragment shaders. So this is basically the boiler plate for this 2D sketch. And we're not going to touch the vertex shader for right now. So we're going to go to the shader, the fragment shader file. Now we don't want just a solid color. What if we change so that it becomes a gradient? So what we can do is that we already have this incoming data from the vertex shader right which is the position of all of the pixels right across the x-axis and the y-axis. So what if we do vose dox? Actually we can just do that right. So vpost.x is already between zero and one. So if we do this, this would range between zero and one. And if I click run, tada. Isn't that amazing? So we're not doing any looping. Like I said before, the pixel itself doesn't actually know what's happening around it, right? All it knows is that what it is like its current XY position in relative to this canvas. This ranges between 0 and one. So let's say like in this exact location here, let's say it is 0.5. So that's why the color here is purplish. But if you go all the way to the left, the position of vost.x is zero, right? And so that's why it becomes blue. And on the very right side here, the value is 1.0. And that's why it's magenta. And then it's a gradient in between. So we can do the same for the second one too. So, let's do V pose.y. Cool, huh? Okay, I get really excited already with just three gradient color. But before we end this video, this first part, I want to make things move a little bit so that it it's exciting before we end here and so that you keep watching to the next episode. So what we're going to do next is we're going to be sending the global variable, right? The dynamic global variable. So basically I'm going to be sending the time and we're going to do that from sketch. We're going to be using this function called set uniform. It sets the shaders uniform global variables. So let's see. We want to put in the name of our shader, right? which we set it as exact same name here my shader and then just do dot set uniform this is the name of the variable that we want to send and then this is the value that it wants to set okay so let's go back my shader dot set uniform and then I'm going to name it u time and then we're going to be sending the function millie which will sense the timer right and actually why don't we print what mill is so you can see what is the data being sent yeah so it just keeps increasing based on the start of the program in milliseconds all right so it's going to be really fast now we know that it's sending that to we're going to send it to the fragment shader. So what we're going to do here is we're going to set the qualifier say uniform. It's going to be a float and it's going to be called u time. Now what we can do is what if I put actually let's set another variable called float x equals to v post.x x + u time. Um going to set this to 0.0 for now. And you just see magenta. If this is zero, it probably was blue before, but it quickly turns magenta because this value here just changes x to more than one very quickly. So what we want to do is we want to cap u time between some range that will make this make sense. So actually I'm going to put in the sign function. So the sign function ranges between -1 and one right. So this value will only be between what is it? So it's between 0 and one. So it's going to be between one and uh two. Yeah. Right. Because at this become if this is zero and then this is negative the most it would be -1 and if this is one and this is one the most it would be two. Okay. So let's try that. Oh, sorry that flickering sketch. Um, we're going to actually divide this by a large number. Let's do 2,00 so that it moves a little bit slower. And you can see there is like that a little bit of this like line kind of like a blown out magenta color. And that is when the value just reaches above one. Right. Okay. Um, yeah. Let's go back. What if we do the exact same thing for y? Actually, I'm going to also set let's call it pulse. Pulse is sign of u time pulse. All right. And then set X and Y here. Pretty nice, huh? Okay, lastly before I go, let's go back to Sketch. What if we change from react to ellipse at width and height and let's set this to 100 details. All right. Okay. I think this is enough of the Hello World shader style and yeah, I will have a few more episodes that will show you more and more of this amazing technique. So stay tuned or go to the next one if it's already up there. We're going to start where we left off last time. So first we're going to see what we have. So we have this moving gradient in a shape of a circle. So, I'm going to actually comment out the ellipse and change it back to a rectangle. And I'm going to make it a little bit boring just for now. But we're going to go to the fragment shader and we're going to actually stop it from animating. So, I'm actually going to delete this line. The pulse is now gone. And also I'm going to change from this beautiful color into a monotone of the grayscale black and white. First I'm going to actually change these values to xx and x and x outputs a value between 0 and one. So if I click run, what you see is a gradient that goes from black to white. And then if I change it to Y, you also see the same gradient, but now it goes from bottom to top. So what I wanted you to see is actually the GISL coordinate space which actually goes from left to right, bottom to top and the origin point is at the bottom left corner here which is a little bit different which is different basically from the P5JS um coordinate space that we're familiar with. Now we want to draw a circle. So how do we do that? In a regular p5 sketch, we would define the center of the circle and then its radius or diameter. But in the case of a shader, it's just asking every single pixel, how far away are you from the center? And use that number to map out the circle. To do that, we can use the glsl built-in function called length to find the distance and pass in the pixels xycoordinates. This will returns the distance between each pixel and its origin point. All right, we're going to try that. But first, we're going to actually delete these. And I'm going to define a new variable of the type ve 2. And I'm going to call it UV. And this is just a conventional name for current space and shaders. And I'm going to set it equals to V pose. And then I'm going to define another variable called D. Set it as a float. and then set it equals to the function length and put in UV and then now inside frack color we're going to put in D comma D comma D all right we see something here and as you can see it also shows us the current space within the GSL canvas right D which ranges between I guess it might be yeah it is between 0 and one because if he poses between zero one and we pass that into the frack color. The distance closest to the origin, the distance would be zero or pretty close to zero, right? So that's why it's black and as far away from the origin as possible, which is this point here. It is completely white. We actually want a circle that has the origin in the middle of the canvas. And we can do that by shifting the coordinate space. So instead of having the coordinate space that ranges between 0 and 1, we want it to be between.5 to positive.5. So this point originally is zero. So all we need to do is subtract it by.5. So we're going to do V post minus 0.5. And ta, now we have a circle. And you can see that we can't see the white color anymore, right? And that is because length here now ranges between -5.5 to positive.5. Let's say we multiply this by 2.0 making it range between -1 and 1. Now we can see a circle. All right. So I'm actually going to move it back to this. Right now to get the position of the square or the canvas, we actually pass in the vertex points from the CPU to the vertex shader through VOST and then we send it into the fragment shader here and we receive it as VOST here through the out and in qualifiers. But actually, we don't need to do that. And the reason that I did it this way is because I wanted to show you how data is being passed through the pipeline. But actually, when it comes to reading screens positions, the GPU already tracks this all under the hood. And you can actually get the coordinates automatically through a built-in variable called GL frack cord. To see where this data comes from, we need to look at the process called rasterization. And rasterization is the process in computer graphics that converts shapes into a 2D grid of pixels to display on the screen. The rasterizer is the specific GPU step that takes those triangles defined by the vertices that the CPU sent and figures out exactly which physical pixels on your monitor fall inside them. First, I'm going to comment this and I'm going to just color this our screen with the color red. All right. And we're going to go back to sketch and we're going to bring back the circle. As you can see here, I put in five arguments and I don't usually do, right? Because what is this last argument here? The fifth parameter detail determines how many vertices are used to draw the ellipse in WebGL mode. And the default value is 25. If I delete this, you can see that the circle is a lot rougher, right? So what if we put in 10? And now we count 1 2 3 4 5 6 7 8 9 10. So basically it gives us the number of vertices based on this number here. So now we have 10. If I put one, you don't see anything because it's just a point. Two, you get a line. Three, a triangle. And then four and so on. Right? But these are actually just a set of triangles stitched together, right? So if we put 25, which is their default setting. If I put 100, now it looks like a circle, right? I mean, it was a circle before, but just a rough circle. Now it's a very smooth circle. This circle basically is just a set of many, many triangles stitched together with a bunch of vertices, right? In this case, 100. So, I'm going to bring back our rectangle. And then inside the vertex shader, I'm actually going to delete the V pose. We don't need it anymore. Okay. And then inside here, the fragment shader. We don't need this either. And then we're going to replace VOST with gl frag cord. But we also only want the x and y coordinates. So we need to doxy. Then click run. Oh, it doesn't nothing happens right now because we need this part here where the UV is being used. It's all white. What's happening here? Exactly what's happening here is that the range for gl_frack cord and vost are actually different. For vpost it ranges between 0 and 1, but for glar cord it actually ranges between our canvas size which is in my case between zero and 400. So what we want to do is that we want to normalize it. So we're going to go to sketch and we're going to actually send the value of the resolution, the width and the height in the form of a uniform. So we're going to do a very similar way to this. So my shader dot set uniform and we're going to call it u resolution. Okay. And then we're going to send it as an array, a 2D vector basically. So we're going to send width and height. And then now inside shader the fragment shaders we just need to receive it right. So uniform we're going to receive it as a ve 2 and it is called urolution. Then now we want to use it right. We want to divide this that goes between 0 and 400 to uro solution.xy which is the width and the height. Then we click run. Hm. That is not what we expected right. We want this circle to be in the middle here and it is supposed to fill the screen. And the reason this is happening is because even though this is the values of 400 comma 400 right the width and the height gl here is actually not between zero and 400. Why? It's because of my screen's resolution. So what we need to do is we're going to go to sketch and we're going to put in a function called pixel density and put in an argument of one. And tada, it works now. So before I move forward, let's just go to the documentation page so you see what's happening. Displays with smaller pixels have a higher pixel density and create sharper images. And I guess in my case, it has a higher pixel density, but we don't want that. We want it to be the same scale. So we can call pixel density with a variable of one to turn this off. So let's quickly recap what we just did. We are using the built-in gl_frack cord variable which tracks raw screen space meaning coordinates running from 0 to 400 on the canvas. Then we normalize the value to range between 0 and one. And we do that by dividing gl frack cord by the total width and height of the canvas which we pass from the CPU as a uniform called U resolution. And we also set the pixel density to one on the p5 side to guarantee our shader coordinates and canvas size maintain a perfect 1:1 ratio. And lastly, we assign this normalized result to the variable UV, which is just a universal convention for current space in the shader world. Next is the exciting part. How are we doing the tiling to create a grid? We don't need to do a lot of different loops that we usually do with columns and rows. We just need to use one built-in function called frack. And what frack does is that it strips away the whole number part of a value and returns only the decimal leftovers. For positive number, the return value is pretty intuitive. But actually for a negative number, you can't just drop the decimal and slap on the negative sign. The actual equation is fractals to x minus floor of x. So for example, frack of.3 returns.7. And if you look at it on a graph, it forms a perfect repeating sawtooth wave that climbs from 0 to one and goes back to zero at every single integer for both positive and negative numbers. And the reason that we can use frack to create a grid is because it basically ignores the whole numbers and provide us with the range between 0 and one over and over and over again. And that just create a repeating pattern that creates a beautiful tile. UV equals to fract UV H doesn't look like what we expected right the reason that it looks like this is because basically this point here you get zero but then this point here you get fract.5 which is.5 and then frack here is.5 is still.5 but in between here it just increases and then becomes zero and It keeps increases again. But now if we multiply it by let's say two, we get something that we expected, right? We have a grid of 2x two. But the circle inside each tile is also not center. And that is because the range is between zero and one. And that's why the color is between black and white from left to right. So we can do the exact same fix here by just subtracting it by 0.5. So that we can remap this values from here to here. So it becomes.5 to.5 and we do that here. And tada. You can multiply the distance here by another constant like we did originally. And you can change this constant here to 3 4 5. So here are the four steps that we did. First we normalize the coordinate values to be between zero and one. And then we center the global coordinates by subtracting UV by.5. And this basically shifts the coordinate space to be between.5 and.5. And then we multiply by a constant of the number of cells that we want. And then we slice it using the function frack. And lastly, we basically do another centering for each of the shapes inside the cell. And we do that the exact same way by subtracting UV by.5. Okay, so I rewrote this to reflect the steps that we just discussed. All right, now let's go back to sketch again. What if I change the canvas size to 600 by 400? Now you can see that our circles are being stretched, right? To fix that, we need to do some aspect ratio adjusting. We're actually going to use this one equation right before all of this. So, we're going to do UV dox just x coordinates multiply equals to u resolution.x divided by u resolution.y. Run. And tada, it magically works. So how does it work exactly? So by dividing the width by the height, so 600 by 400, the shader calculates a scaling ratio of 1.5. And this tells us the screen is exactly 1.5 times wider than it is tall. And without this fix, both axis are forced into a strict 0 to1 range. And by multiplying UV.x X by that 1.5 fraction. We expand the range of the X-axis to match the physical shape of the monitor. The Y-axis stays locked between 0 and 1, but the X-axis now stretches out from 0 to 1.5. But notice what this does to our center point. Since our width is between 0 and 1.5, the middle point is now 0.75. By subtracting a hard-coded value of 0.5, our visuals are now offcentered. To fix this, we can calculate the shifts for both the x and y axis independently. For the x-axis, we can subtract half of the aspect ratio to account for that extra width. For the y-axis, since it spans from 0 to 1, we can safely subtract it by 0.5. And that's it. And that's how you create the beautiful tiling inside shaders. But before I end here, I want to point out one final thing. If you open up sites like shader toy or you look up some other resources online, the first line of mini fragment shader might look like this. And don't be scared because you have seen it before. So if you look at our vertex shader file here, you can see that position.xy here is also multiplied by two and subtract by one. The only difference is that the the scaling is different here. The range is between 0 and one and we want to map it to negative 1 and one. So we multiply it by two and then we subtract it by one. But in the case of the GL frack chord which ranges between 0 and 400 what we want to do is that we want to multiply it by two and then we want to subtract it by the width basically right and then the divide by u resolution.y at the end here is the exact same thing that we just discussed. But you can see that it looks like there are many more cells here and you are exactly right. That is because now the range that this provides is between negative 1 and one whereas how we did it before the range was between.5 and.5. So this basically doubles the grid size. And that is it for this video. I hope you stick around. Let's go. So this is where we left off in the last episode. I want to make some changes before we move forward. So first I want to change the size of the canvas to 400x 400. Okay. And then we're going to go to the fragment shaders. And then remember we talked about the two different ways to get the UV coordinates. I'm actually going to use this equation here. And I'm going to delete this first part. All right. And then I want to change the constant here to 2.0 so that we have a 4x4 grid. So next we're going to talk about a shaping function. So what is a shaping function? A shaping function is any math function that takes in a number and sculpts it into a different shape. And to talk about the shaping function, what I want to do is I actually also want to comment this out. So instead of working with a 2D space, why don't we start with a 1D space so that it's easier to grasp the concept. So, I'm going to create a variable called X. And we're going to set X to be equals to the UV dox, which is the X component of the UV coordinate. And then I'm going to change this to X comma X, X. The first shaping function that we're going to play with is the sign function. Let's put in sign of UV.X. What do you notice here? Maybe you can see it more if I multiply it by a bigger number. It seems like the black band is bigger than the white band, right? And that is because if I delete this, that is because if you look here, the range here is between -1 and one, right? The range of the UV current space. And when you put it inside here inside signed which oscillates between value of -1 and one the color that is being rendered is actually only is between 0 and one. So any negative number that is put as an input returns a value of zero. So the GPU basically just clips the negative number as zero. And so that's why anything that is on the left side where the range is between negative one and zero is black. And then anything on the right side now just returns values between 0 and one. And because sign of one is around8 you can see that this point here is light gray. Now what if we want a clean edge or a hard cut off between black and white. We can actually use another shaping function called step. And step takes in two arguments. The first one is the threshold and then the second one is the input value. X equals to step of 0.0, X. 0.0 is the threshold. So anything below zero will return a value of zero. Anything above zero will return a value of one. And as you can see here, anything below zero is the negative number here. So the left side should be black and the right side should be white. Right? Let's give it a try. And tada. You get a clean cut between black and white. But what if you think that this is a little bit too jarring? You want a little bit of a transition between the two. You can actually use another shaping function called smooth step to get that smooth transition. It requires one additional argument. I'm going to comment this out and then just put in x equals to step no smooth step of how about we do negative0.5 0.5 and then x and then now you can see that there is this nice transition between the two. So basically we provide two arguments the lower threshold and then the upper threshold and anything that is below the lower threshold returns zero anything that is above the upper threshold returns one and anything in between gets a nice transition as you can see here. So if we provide a smaller band here. So.1.1 and 0.1 you can see that this transition in between is smaller. But to create the glowing ring we're actually not going to be using either of these shaping functions. We're going to be using ABS or absolute. We don't want to clip out any of the negative values. We want to fold the negative value to be a positive value. And that's how we actually create those glowing rings. X equals to absolute value of X. Now the negative values basically just mirror the positive side. Instead of just providing UV.X and you just get this one set of gradients, right? What if we multiply it by a multiple of pi? Let's start with this pi. 3.14. You can see that in the middle point here is sine of 0 which returns zero and you get this black band. And then at this point here which is sine of pi / 2 you get a color white because it returns one. And then at the right edge here which is sine of pi which equals to zero. And then you get another black edge here. Same thing on the left side. Right? Now that we put in the absolute value, we get the mirror image of the right side and then you can multiply it by other constants to get multiple periods. So I'm actually going to do four. Why don't we bring back our 2D space? So let's uncomment this. And what we need to do is we need to now set D to be equal to the sign function here. And then also set D here. Change all the X to D. Okay. All right. That looks kind of nice. Now to the most exciting part, at least for me, is to create those glowing rings. How do we do that? We do it using this equation that mimics how light behaves. This is the inverse distance equation. So we basically divide a small constant by our distance value. All right, let's give this a try. So we're going to do d = 2 n / d, right? And we can pick our n. So I'm going to start with 0.1 / d. All right. What if we increase the value of the numerator here? Ooh, it glows a lot more. And how about we decrease it to 0.05? Okay, how about even smaller? So, how does this work exactly? If you look here where the sharp values are, this is where the absolute function brings the wave to zero, right? And so by dividing a number extremely close to zero, the output basically is just going towards infinity and the GPU just cap it to one which is the color white. And as you get away or get further from the value, the distance d gets bigger and the output is closer to zero and the brightness falls off quickly. And as we modify the numerator, you could see that the size of the glowing rings changes, right? And that is because the numerator literally is the distance at which you get the full brightness. Let's say you set it to 0.02, the only pixels within that distance, 0.02, are the ones that are going to be fully white and everything else will fall off or is black. If you set it to a different number, let's say 0.1 or 0.5, that full brightness zone just get wider. And here's the part I find most fascinating. This is how light actually behaves. Brightness falls with distance from a source. In physics, there's actually a law called an inverse square law. And the equation is 1 / d². But in shaders, we're using 1 / d because it looks right and it costs less. And there's even a name in painting. It's called chia rose kuro. I believe this is Italian. So any Italian audience out there, correct my pronunciation. It is the contrast between light and dark that creates the illusion of luminosity and you've seen it in many famous paintings. Next, we're going to bring back some animation. So, remember the u time uniform variable that we used in the first episode. Let's bring that back. So inside sketch we're going to do my shader dot set uniform and then the variable is going to be named u time and we're going to send the millie function which basically just counts the number of milliseconds from the start of the program and we're going to divide it by a large constant so that it doesn't move too fast. So I'm going to do actually how about a thousand and we're going to go to the fragment shader and then here we're going to receive it as a uniform and it is a float called u time and we're actually going to provide it inside the sign function here. Let's do plus u time and actually going to change this to 0.1. All right. And now we have this moving glowing rings. So basically we provide this u time inside the sign function so that it changes the value inside the wave. Right? It basically shifts the sign function in time. Right? So if we add it you can see that it moves in one way and then if we subtract it then we get a different movement. So it just the different ways that we shift that sign function that sign oscillating waves. We have been working with the circles and as exciting as that is it is pretty exciting. We're going to change to a different shape. All we need to do is we just need to change this function here. An inch heater is basically any shape is just a function that returns a distance. And that's where the fun begins because when we swap out the function, we get a different shape. So let's start simple. Instead of circle, we're going to work with an ovel. So instead of length of UV, what if we multiply this by a big two of let's do how about 1.0. 0 and 1.5. So we change the y-coordinate here. What do you expect? All right. So we have this more ovalshaped rings. And what we want is actually a function that creates a shape of a heart. I'm going to copy and paste this equation here. And then we're going to come to this line and instead of d equals to this we're going to do d equals to heart of uv and tada. And you see all this equation and you're like pat. You're not supposed to know it. You're supposed to go and look them up. So I'm going to provide some resources and then just try a few so that you can see how you can play around with it. So if you come to this website here by the great Ino Kilis, he has a bunch of the 2D distance functions. So we're going to look at a few of them. Why don't we work with let's just do a rounded box. So I'm going to just copy this and come here float of SD rounded box and let's just copy here. I'm going to comment out our heart and float of D to be equals to this. And then the vector 2 of P will just be our UV coordinate. The vector of B, I don't know what should it be. How about we just do 0 1.0 and vect of 4. Let's do 1.0 as well. And you can see that I only provided just one value. And with vectors within GLSL, this basically is going to populate it for me. So it's going to be 1.0 for all four values for this ve 4 and then two values for this ve two. Hm. It just looks like a circle. So, what if I change this to two? Oo, nice. How about we do 1.0 and 2.0 here. All right. So, as you can see, you can play around with these equations, but I'm actually going to comment this and just end with our heart. And that's it for this video. In the next video, I'm going to show you how to add some nice colors and also we're going to be using a for loop to wrap all of these shapes inside. Let's go. Talking about colors, let me introduce you to this function called mix. Mix takes in a total of three arguments. The two colors and amount. The amount ranges between 0 and one. So, if you put in zero, you get the color A. If you put in one, you get the color B. And if you put anything in between, you get the mix between those two colors. So here is where we left off in the last episode. I'm going to define two colors as a ve 3. So let's do vec 3. Name it color a equals to ve 3 of how about we do a color red. So 1.0.0 and 0.0. And then I'm going to do color B as the color how about blue? RGB. So 0.0.0 and 1.0. zero. And then inside the main function here, we're going to define the amount. So let's set it as a float amt equals to how about 0.5. And we're going to set a new variable as a vetory called color. And color is going to be set to the function mix. And we're going to put in color A, color B, and AMT. And we're going to use this color inside frack color here. So we're going to replace D here with color. All right. What do you expect? So we're mixing red and blue. And tada, we get this purple. Okay. So we can now play around with the amount here, right? So if I do 0.0, Z, you should get the color red. And if I do 1.0, you get blue. And anything between, let's try 0.3, you get something closer to the red side. Why don't we actually use the sign function, which outputs values between -1 and 1. And then we're going to pass in a variable u time, which is our timer. Right? This will give us an amount between -1 and one. So let's click run. Okay. If you notice, you can see that the color red is pausing a little bit longer than the blue. And that is because the amount actually only ranges between 0 and one, right? But this returns values between negative 1 and one. And with the negative one value, it is capped as the color red. So we're going to actually put in absolute to make sure that we just turn the negative value to positive. So now it's within the range of zero and one. But then now we can only get this one color each time, right? Each frame. So what if we want to get that nice gradient? What we can do is we can actually map it to our UV coordinate. How about we map it to the X coordinate? What we're going to do is that we're going to set AMT here. I'm going to delete this. Set it to how about UV of X. And you get a nice gradient. But you can also see that we get a much larger part that is red actually up into half, right? And then the rest is this nice gradient. Basically, this UV coordinate ranges between negative 1 and one, right? So, it's the exact same thing. I don't want to touch this line of code. So, I'm going to do something that is redundant, which is to remap it. So, how about we do UV of X plus 1.0. So, now it will range between 0 and 2. And then we're going to divide it by two. So, now it would range between 0 and one. So now we have a nice gradient that is evenly distributed. Now the mix function can also take in not just a float but a ve 3. So we're going to change amt to a ve 3 and this will be a ve 3 as well. And if we just change the type right now it would look exactly the same. And that is because all of the channels red, green, and blue have the same blending ratio which is based on the x coordinate. Right? So what we can do is that we can actually change how about we change the r channel. How about we set it to a decimal a number? Um let's do 0.1. And we get a different gradient, right? Gradient color. But this doesn't really show you the magic of how if we were to change the blending ratio of each channel, what can we do, right? What if we bring back what we learned in the last episode, which are the shaping functions? I'm going to add in how about step and step basically returns either zero and one based on the threshold value, right? So if I set the threshold value at 0.5 and the values that we want to change is the red channel of this amount and let's try it. Tada. So you get this nice clean cut, a really hard cut off between the two colors. But you can still see that there's a bit of a gradient here, right? From red to pink and then like a blue, a darker blue to like a lighter blue here. And that is because we're only changing the blending ratio of the red channel, but the green and the blue channels are still based on this amount that we have here. So this is just to show you that you can bring in shaping functions to change the different amounts of each of the color channels and just play around with that which is kind of fun. Now let's make it a little bit more interesting. What if we bring in the distance functions that we learned in the last episode as well creating like a circle or a heart shape into manipulating these color gradients. So, I'm going to delete this. Actually, instead of setting amount to this X coordinate, why don't we set it to how about length of UV? And then we need to set it as a float. And let's run. Tada. You get a circular shape. Basically the closest to the center here returns a value of zero, right? And as you move out, then you get this nice blue color. And we can change it to a heart shape also. And this heart shape, what is interesting about this is that it actually is assigned distance function. meaning that anything that is inside the shape returns a negative value and anything outside the shape returns a positive value. So if you really notice you can see a pretty hard cut off of this heart shape here that is completely red. So what if we actually bring back the step function? So we do step of 0.0 zero and then heart here. And you should see a clean cut heart shape. And we can change the step to something a little bit smoother, right? So we can do smooth step and we can change it between how about so if this is zero, what if we do it from here to like here? So negative 0.3 to 0.3. And now you get a nicer, smoother blend between the two colors. So you can play around with that. So now let's bring back the grid of hearts that we created earlier. To do that, basically instead of just putting in color as the values inside the frack color here, what we want to do is that we want to multiply it by D. And the reason that we want to multiply it by D is because Dictates the brightness of the color that is to be painted. Right? So where D is the highest here is where the shape of the heart is. So what we're going to do is we're going to just multiply it by D. Now we get the shape that we created originally. And I'm going to actually change this a bit. I'm going to change inside sign here. Let's do a nice heart shape. Just one shape here. And yeah, that's it. Up until this point, we experimented with a few ways to come up with the amount value, right? By passing in the time, the position, or using shaving functions. But we're basically doing the same thing. We're picking a value or a color or creating a gradient that is between two colors, color A and color B. But now I want to introduce you to this cosinebased equation which creates a richer spectrum of colors. This function takes in a float and returns a color. And the four vex 3es that you see here, A, B, C, and D, basically shape the bass color, the contrast, the oscillation frequency, and phase offset per channel. And I'll link the article and also a video describing how this works, as well as interactive tools that you can try to create your own palette. So, I've already played around with the palette. So, I'm going to just copy and paste here outside of the main function and inside the main function here instead of all of this. We're going to set color to the palette. And so, how about I delete the amount here. We don't need it anymore. We're not going to use the mix function. We're going to use pallet. And then we're going to put in let's just start with length of UV. And I'm actually also going to change this back to color. All right. This is nice. Okay. So, how about we change to heart? And then what if we make it move by adding you time? This reminds me of the intro to Powerpuff Girls. And you can also, you know, if you want it to be slower, faster, you can multiply by a constant. And now what if we bring back the shape? Just multiply this by D. All right. And I want the the the um the heart inside the grid here to be a little bit brighter. So I'm going to increase the enumerator here. And as you can see, this is pretty much the final piece. But these are actually the exact variables that I used. So you can play around with that to get the one that you want. But I'm not actually going to end here. I'm going to give you a little bit of a bonus. So, as I mentioned in the second episode that this final piece is actually a direct remix of a piece by Kishimisu, but as you can see from his piece, it looks like there's just a bunch of circles on top of each other. So, this is the last feature that I want to introduce you how to create a fractal. So, on the CPU, you might be building this using recursion, but on the GPU, you cannot actually do that. So instead we would be remapping the UV coordinate using a for loop. So I'm going to wrap the for loop here. So for it's going to be a float. So float I equals to 0.0 I less than how about 2.0. Let's start small. I ++ and then we're going to bring in everything here inside the for loop. Okay. Um let's see what happens. Okay. So now we have what? 1 2 3 4 5 6 7 8. We have 8 by 8, right? Because we are running just one set of I mean two sets of for loop. But we can only see just one set of scale, right? which is the last set 8 by 8 here. So what we want is actually we want to be able to see all the shapes on top of each other. We want to see the different scales. So what we can do is that we can actually manipulate the colors. So right now frack is being set here. What if we accumulate the colors? So what we're going to do is outside of the for loop here, I'm going to define a ve 3 called final color and set it originally as black. So 0.0. And then inside the for loop here, we're going to set final color as here. Color time D. And we're going to set frack color outside of the for loop. So this will be the final final color. If we just set it as final color equals to color time d, what you would expect is oops um let's see final color is a ve three. Okay, it looks exactly the same because the color is being set to this each pass. So what we want to do is that we actually want to accumulate it. So we start off as black and then each of the fold right each of the remapping will have a different final color. It will be accumulating. So let's try it. Very cool. I think I'm just going to end here. And as you can see, you can play around with the different variables. You can bring in the shaping functions. You can accumulate different colors together. Use the technique of remapping the UV coordinate and create a pretty cool final piece like this one. This is so mesmerizing. All right. So, this is the end of this introductory series. I hope that you enjoyed it and learned a few things from a new beginner like myself as well. There's still a lot that I want to learn. So, please let me know what are some of the things that would be need to explore. See you next time. Bye.

Generated algorithmically for Search Engine Indexing.

Summarize Another Video