practice

Before we draw our first 2D shape, let’s practice a little more with Shadertoy. Create a new shader and replace the starting code with the following:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >

  vec3 col = vec3(0); // Start with black
  
  if (uv.x > . 5) col = vec3(1); // Set the right half of the canvas to white

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

Since our shader runs in parallel on all pixels, we have to rely on if statements to draw pixels of different colors based on their position on the screen. Depending on your video card and the compiler used for your shader code, it may be more efficient to use built-in functions such as step.

Let’s look at the same example, but use the step function instead:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >

  vec3 col = vec3(0); // Start with black
  
  col = vec3(step(0.5, uv.x)); // Set the right half of the canvas to white

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

The left half of the canvas is black and the right half of the canvas is white.

A step function takes two inputs: the edge of the step function and the value used to generate the step function. If the second argument in the function argument is greater than the first argument, the value 1 is returned. Otherwise, zero is returned.

You can also perform step functions on each component of a vector:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >

  vec3 col = vec3(0); // Start with black
  
  col = vec3(step(0.5, uv), 0); // Execute the step function across the X and y components of the UV

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

Since the step function works on both the X and Y components of the canvas, you should see the canvas divided into four colors.

How to Draw a circle

The equation of a circle is defined as follows:

x^2 + y^2 = r^2

x = x-coordinate on graph
y = y-coordinate on graph
r = radius of circle
Copy the code

We can rearrange the variables so that the equation equals zero:

x^2 + y^2 - r^2 = 0
Copy the code

To visualize this on a chart, you can use a Desmos calculator to draw the following chart:

x^2 + y^2 - 4 = 0
Copy the code

If you copy the code snippet above and paste it into your Desmos calculator, you should see a circle with radius 2. The center of the circle is at the coordinate 0, 0.

In Shadertoy, we can use the left-hand side of the equation (LHS) to make a circle. Let’s create a function called sdfCircle that returns the color white for each pixel on the XY coordinate, making the equation greater than zero and otherwise blue.

The SDF part of the function refers to a concept called the signed distance function (SDF), which is the signed distance field. It is more common to use SDF when drawing in 3D, but I would also use this term for 2D shapes.

We’ll use it by calling our new function mainImage inside the function.

vec3 sdfCircle(vec2 uv, float r) {
    float x = uv.x;
    float y = uv.y;
    
    float d = length(vec2(x, y)) - r;
    
    return d > 0. ? vec3(1.) : vec3(0..0..1.);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >
  
  vec3 col = sdfCircle(uv, 2.); // Call this function on each pixel to check whether the coordinates are inside or outside the circle

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

If you’re wondering why I use 0. Rather than simply 0 without decimals, it’s because adding decimals to the end of integers gives it the type float instead of int. When you use functions that require numbers of type float, putting decimals at the end of integers is the easiest way to satisfy the compiler’s requirements.

We use radius 0.2 because our coordinate system is set to have only UV values between 0 and 1. When you run the code, you will notice that some errors occur.

There appears to be a quarter of a blue dot in the lower left corner of the canvas. Why is that? Because our coordinate system is currently set with the origin in the lower left corner. We need to shift each value by 0.5 to get the origin of the coordinate system at the center of the canvas.

0.5 Subtract from the UV coordinates:

vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >
uv -= 0.5; / / < 0.5, 0.5 >
Copy the code

The range is now between the x and y axes -0.5, 0.5 which means that the origin of the coordinate system is in the center of the canvas. However, we are faced with another problem……

Our circle looks a little elongated, so it looks more like an ellipse. This is caused by the aspect ratio of the canvas. When the width and height of the canvas do not match, the circle will appear stretched. We can solve this problem by multiplying the X component of the UV coordinate by the aspect ratio of the canvas.

vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >
uv -= 0.5; / / < 0.5, 0.5 >
uv.x *= iResolution.x/iResolution.y; // Fixed aspect ratio
Copy the code

This means that the X component is no longer between -0.5 and 0.5. It will vary between values proportional to the aspect ratio of the canvas, depending on the width of the browser or page (if you use something like Chrome DevTools to change the width).

Your completed code should look like this:

vec3 sdfCircle(vec2 uv, float r) {
  float x = uv.x;
  float y = uv.y;
  
  float d = length(vec2(x, y)) - r;
  
  return d > 0. ? vec3(1.) : vec3(0..0..1.);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >
  uv -= 0.5;
  uv.x *= iResolution.x/iResolution.y; // Fixed aspect ratio
  
  vec3 col = sdfCircle(uv, 2.);

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

After you run the code, you should see a perfectly proportional blue circle

We can have some fun with it! We can use the global iTime variable to change color over time. By using the cosine function, we can loop through the same set of colors over and over again. Since the cosine oscillates between -1 and 1, we need to adjust this range to the value of -1 between 0 and 1, 1.

Remember that any color value less than zero in the final fragment color will automatically be limited to zero. Again, any color value greater than 1 will be limited to 1. By adjusting the range, we can get a wider range of colors.

vec3 sdfCircle(vec2 uv, float r) {
  float x = uv.x;
  float y = uv.y;
  
  float d = length(vec2(x, y)) - r;
  
  return d > 0. ? vec3(0.) : 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0.2.4));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >
  uv -= 0.5;
  uv.x *= iResolution.x/iResolution.y; // Fixed aspect ratio
  
  vec3 col = sdfCircle(uv, 2.);

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

After you run the code, you should see the circles change between various colors.

We can use the components of variables to create new vectors. Let’s look at an example.

vec3 col = vec3(0.2.0.4.0.6);
vec3 col2 = col.xyx;
vec3 col3 = vec3(0.2.0.4.0.2);
Copy the code

In the code snippet above, COL2 and col3 are identical.

Move the circle

To move the circle, we need to apply offsets to the XY coordinates in the circle equation. Therefore, our equation will look like this:

(x - offsetX)^2 + (y - offsetY)^2 - r^2 = 0

x = x-coordinate on graph
y = y-coordinate on graph
r = radius of circle
offsetX = how much to move the center of the circle in the x-axis
offsetY = how much to move the center of the circle in the y-axis
Copy the code

You can try it out again in the Desmos calculator by copying and pasting the following code:

(x - 2)^2 + (y - 2)^2 - 4 = 0
Copy the code

In Shadertoy, we can adjust the sdfCircle function to allow the offset and then move the center of the circle by 0.2.

vec3 sdfCircle(vec2 uv, float r, vec2 offset) {
  float x = uv.x - offset.x;
  float y = uv.y - offset.y;
  
  float d = length(vec2(x, y)) - r;
  
  return d > 0. ? vec3(1.) : vec3(0..0..1.);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >
  uv -= 0.5;
  uv.x *= iResolution.x/iResolution.y; // Fixed aspect ratio
  
  vec2 offset = vec2(0.2.0.2); // Move the circle 0.2 to the right and 0.2 to the up
  
  vec3 col = sdfCircle(uv, 2..offset);

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

You can iTime again in some places using global variables to animate the canvas and animate your circles.

vec3 sdfCircle(vec2 uv, float r, vec2 offset) {
  float x = uv.x - offset.x;
  float y = uv.y - offset.y;
  
  float d = length(vec2(x, y)) - r;
  
  return d > 0. ? vec3(1.) : vec3(0..0..1.);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; / / < 0, 1 >
  uv -= 0.5;
  uv.x *= iResolution.x/iResolution.y; // Fixed aspect ratio
  
  vec2 offset = vec2(sin(iTime*2.) *0.2.cos(iTime*2.) *0.2); // Move the circle clockwise
  
  vec3 col = sdfCircle(uv, 2..offset);

  // Output to the screen
  fragColor = vec4(col,1.0);
}
Copy the code

The code above moves the circle clockwise along the circular path as if it were rotating around the origin. By multiplying iTime by a value, you can speed up the animation. You can control how far the circle moves from the center of the canvas by multiplying the output of the sine or cosine function by a value. You’re going to use a lot of sines and cosines, iTime because they oscillate.

conclusion

In this lesson, we learned how to fix a canvas’s coordinate system, draw a circle, and animate a circle along a circle’s path. In the next lesson, I will show you how to draw a square on the screen. And then we’re going to learn how to rotate it, right