In this article, I want to discuss a better way to draw 2D shapes so that we can easily add multiple shapes to the canvas. We will also learn how to change background colors independently of shape colors.

Mixed function

Before continuing, let’s take a look at the blending features. This feature is especially useful when we are rendering multiple 2D shapes into a scene.

The Mix function interpolates linearly between two values. In other shader languages, such as HLSL, this function is called Lerp.

The linear interpolation of the function Mix (x, y, a) is based on the following formula:

X * (1-a) + y * a x = the first value y = the second value a = the linear interpolation between x and yCopy the code

Think of the third argument, A, as a slider that lets you choose the value y between x and.

You’ll see that Mix is a heavily used function in shaders. This is a great way to create color gradients. Let’s look at an example:

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

    float interpolatedValue = mix(0..1., uv.x);
    vec3 col = vec3(interpolatedValue);

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

In the code above, we use the Mix function to get the interpolation for each pixel on the X-axis of the screen. By using the same values on the red, green, and blue channels, we get a gradient from black to white, with a gray shadow in the middle.

We can also use mix as a function along the Y-axis:

float interpolatedValue = mix(0..1., uv.y);
Copy the code

Using this knowledge, we can create color gradients in pixel shaders. Let’s define a function that specifically sets the background color of the canvas.

vec3 getBackgroundColor(vec2 uv) {
    uv += 0.5; // adjust the uv range <-0.5,0.5> to <0,1>
    vec3 gradientStartColor = vec3(1..0..1.);
    vec3 gradientEndColor = vec3(0..1..1.);
    return mix(gradientStartColor, gradientEndColor, uv.y); // Gradient from bottom to top
}

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

    vec3 col = getBackgroundColor(uv);

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

This produces a cool gradient between purple and cyan.

When MIX uses this function on vectors, it interpolates each vector on a component basis using a third argument. It will run the gradientStartColor vector’s red component (or X component) and the vector’s red component’s interpolator function gradientEndColor. The same strategy will be applied to the green (y component) and blue (Z component) channels of each vector.

We adjusted the value of UV because, in most cases, we will use values between negative and positive UV. If we pass a negative value to fragColor, it will be pinched to zero. To show the color across the entire range, we move the range away from negative values.

Another way to draw 2D shapes

In previous tutorials, we learned how to create 2D shapes using 2D SDF, such as circle sdfCircle and square sdfSquare. However, the function returns the color in the form of vector vec3.

Typically, the SDF returns float instead of vec3. Remember, “SDF” is an acronym for “signed distance field.” Therefore, we want them to return a distance type float. In 3D SDF this is usually true, but in 2D SDF I find it more useful to return 1 or 0 depending on whether the pixel is inside or outside the shape, as we’ll see later.

Distance is relative to a point, usually the center of the shape. If the center of a circle is at the origin (0, 0), then we know that any point on the edge of the circle is equal to the radius of the circle, so the equation is:

X ^2 + y^2 = r^2 or, rearrange them x^2 + y^ 2-r ^2 = 0 x^2 + y^ 2-r ^2 = distance = dCopy the code

If the distance is greater than zero, then we know we’re outside the circle. If the distance is less than zero, then we’re inside the circle. If the distance is exactly zero, then we are at the edge of the circle. This is where the “signed” part of the “signed distance field” comes in. The distance can be negative or positive, depending on whether the pixel coordinates are inside or outside the shape.

In chapter 2 of this tutorial series, we drew a blue circle using the following code:

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.);
  // If you draw a background color outside the shape
  // If inside the shape, draw a circular color
}

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

The problem with this method is that we have to draw a circle in blue and a background in white.

We need to make the code a little more abstract so that we can draw the background and shape colors independently of each other. This will allow us to draw multiple shapes in the scene and choose any color we want for each shape and background.

Let’s look at another way to draw blue circles:

float sdfCircle(vec2 uv, float r, vec2 offset) {
  float x = uv.x - offset.x;
  float y = uv.y - offset.y;

  return length(vec2(x, y)) - r;
}

vec3 drawScene(vec2 uv) {
  vec3 col = vec3(1);
  float circle = sdfCircle(uv, 0.1.vec2(0.0));
  
  col = mix(vec3(0.0.1), col, step(0., circle));
  
  return col;
}

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

  vec3 col = drawScene(uv);

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

In the code above, we are now abstracting something. We have a drawScene function that renders the scene, and sdfCircle now returns a float that represents the “signed distance” between a pixel on the screen and a point on the circle. We learned about step functions in Chapter 2. It returns the value 1 or 0 depending on the value of the second argument. In fact, the following is equivalent:

float result = step(0., circle);
float result = circle > 0. ? 1. : 0.;
Copy the code

If the signed distance value is greater than zero, the point is inside the circle. If the value is less than or equal to 0, the point is on the outside or edge of the circle.

Inside the drawScene function, we use the mix function to mix the white background color with the blue. The value circle determines whether the pixel is white (background) or blue (circle). In this sense, we can use the mix function as a “toggle,” switching between shape colors or background colors based on the value of the third parameter.

Using SDF in this way basically allows us to draw shapes only when the pixels are at coordinates within the shape. Otherwise, it should draw the previous color.

Let’s add a slightly off-center square.

float sdfCircle(vec2 uv, float r, vec2 offset) {
  float x = uv.x - offset.x;
  float y = uv.y - offset.y;
  
  return length(vec2(x, y)) - r;  
}

float sdfSquare(vec2 uv, float size, vec2 offset) {
  float x = uv.x - offset.x;
  float y = uv.y - offset.y;

  return max(abs(x), abs(y)) - size;
}

vec3 drawScene(vec2 uv) {
  vec3 col = vec3(1);
  float circle = sdfCircle(uv, 0.1.vec2(0.0));
  float square = sdfSquare(uv, 0.07.vec2(0.1.0));
  
  col = mix(vec3(0.0.1), col, step(0., circle));
  col = mix(vec3(1.0.0), col, step(0., square));
  
  return col;
}

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

  vec3 col = drawScene(uv);

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

Using the mix function in this way allows us to easily render multiple 2D shapes into the scene!

Custom background and multiple 2D shapes

With what we’ve learned, we can easily customize the background while keeping the shape and color the same. Let’s add a function that returns the background gradient color and use it to drawScene at the top of the function.

vec3 getBackgroundColor(vec2 uv) {
    uv += 0.5; // adjust the uv range <-0.5,0.5> to <0,1>
    vec3 gradientStartColor = vec3(1..0..1.);
    vec3 gradientEndColor = vec3(0..1..1.);
    return mix(gradientStartColor, gradientEndColor, uv.y); // Gradient from bottom to top
}

float sdfCircle(vec2 uv, float r, vec2 offset) {
  float x = uv.x - offset.x;
  float y = uv.y - offset.y;
  
  return length(vec2(x, y)) - r;
}

float sdfSquare(vec2 uv, float size, vec2 offset) {
  float x = uv.x - offset.x;
  float y = uv.y - offset.y;
  return max(abs(x), abs(y)) - size;
}

vec3 drawScene(vec2 uv) {
  vec3 col = getBackgroundColor(uv);
  float circle = sdfCircle(uv, 0.1.vec2(0.0));
  float square = sdfSquare(uv, 0.07.vec2(0.1.0));
  
  col = mix(vec3(0.0.1), col, step(0., circle));
  col = mix(vec3(1.0.0), col, step(0., square));
  
  return col;
}

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

  vec3 col = drawScene(uv);

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

conclusion

In this lesson, we learned how to use the Mix function to create color gradients, and how to use it to render shapes on top of each other or on top of background layers. In the next lesson, I’ll discuss other 2D shapes we can draw, such as hearts and stars.