Learn with me: Getting Started with Shader Functions (step, mix, smoothstep)

Introduction: Learning Shaders Together!

Hey everyone, welcome back to my "Learn with me" series! In the last post, we dipped our toes into the very basics of GLSL shaders. Today, we're diving a bit deeper and exploring some fundamental shader functions. These are the building blocks that let us control pixels and create cool visual effects.

One of the best parts about this article (and hopefully this series!) is that it's interactive. You'll find live shader editors embedded right here on the page for most examples. Feel free to jump in, tweak the code, change values, and see the results update instantly! You can even use these editors to experiment and build your own little shader creations. For the challenges, your code progress will be automatically saved in your browser's local storage, so you don't have to worry about losing your work if you step away.

Quick tip: If the preview suddenly turns black, it usually means there's an error in your shader code. Don't panic! Just open your browser's developer console (usually by pressing F12) – you'll likely find helpful error messages there pointing you to the problem line.

Full disclaimer: I'm still very much learning shaders myself! Think of this series as my public learning journal. The examples here are meant to be simple and illustrative, probably not the most optimized or technically perfect code you'll ever see. My goal is to understand these concepts and share that journey with you. If you spot mistakes or have better ways of doing things, please share with me – we can all learn together!

The awesome thing about shaders is that a lot of effects boil down to math. Once we grasp a few core functions, we can start combining them in creative ways. Let's get hands-on with some interactive examples!

Why Bother with Shader Functions?

Think of the fragment shader as a tiny program that runs for every single pixel on your geometry. Functions like step, mix, and smoothstep give us precise control over the color (or other properties) of each pixel based on its position, distance, or other calculations.

Essential Shader Functions: Our First Tools

Let's explore three super useful functions.

1. The step Function: Creating Hard Edges

The step function is like a simple switch. It compares two values and returns either 0.0 or 1.0.

  • Syntax: step(edge, x)
  • How it works:
    • If x is less than edge, it returns 0.0.
    • If x is greater than or equal to edge, it returns 1.0.

It's perfect for creating sharp divisions or masks.

Example: A Simple Vertical Line

Let's divide the plane in half vertically using vUv.x (the horizontal texture coordinate, which goes from 0.0 on the left to 1.0 on the right).

Try it yourself:

  1. Change the edge value (e.g., to 0.2 or 0.8) to move the dividing line.
  2. Try using vUv.y instead of vUv.x to create a horizontal line.
  3. Can you make the left side white and the right side black? (Hint: You might need to swap something)

2. The mix Function: Blending Between Values

The mix function (also known as lerp or linear interpolation) is fantastic for blending between two values.

  • Syntax: mix(x, y, a)
  • How it works: It returns a blended value between x and y. The amount of blending is controlled by a.
    • If a is 0.0, it returns x.
    • If a is 1.0, it returns y.
    • If a is 0.5, it returns a perfect 50/50 mix of x and y.
    • Values between 0.0 and 1.0 give proportional blends.

Example: A Simple Horizontal Gradient

Let's blend between two colors using vUv.x as the blending factor.

Try it yourself:

  1. Change colorA and colorB to create different gradients.
  2. Use vUv.y as the blend factor for a vertical gradient.
  3. What happens if you use something like vUv.x * 0.5 as the blend factor?

Challenge #1: Creating a Four-Quadrant Color Grid

Okay, first challenge! Let's combine step and mix to create a grid with four different colors. Use step to define the horizontal and vertical dividing lines, and then use mix (maybe nested?) to select the correct color for each quadrant.

Goal:

  • Top-Left: Red
  • Top-Right: Green
  • Bottom-Left: Blue
  • Bottom-Right: Yellow
Saved

3. The smoothstep Function: Creating Smooth Transitions

While step gives us hard edges, smoothstep creates a nice, smooth transition between 0.0 and 1.0.

  • Syntax: smoothstep(edge0, edge1, x)
  • How it works:
    • If x is less than edge0, it returns 0.0.
    • If x is greater than edge1, it returns 1.0.
    • If x is between edge0 and edge1, it returns a smoothly interpolated value (using a Hermite curve) between 0.0 and 1.0.

It's perfect for soft gradients, fades, or feathered edges.

Example: A Smooth Gradient

Let's create a smooth transition between two colors.

Try it yourself:

  1. Change edge0 and edge1 to see how the transition width and position change.
  2. What happens if edge0 is greater than edge1? (e.g., smoothstep(0.7, 0.3, vUv.x))
  3. Can you make a smooth vertical gradient?

Applying smoothstep: Vignette Effect

A common use for smoothstep is creating a vignette (darkening the corners/edges). We can calculate the distance of each pixel from the center and use smoothstep to make it darker further away.

Challenge #2: Modifying the Vignette

Using the vignette code above as a starting point, can you:

  1. Make the vignette effect oval instead of circular? (Hint: You need to adjust how dist is calculated. Maybe scale the x or y component of the vector before calculating the distance? Or calculate dx and dy separately and combine them differently.)
  2. Make the vignette sharper (less smooth)? (Hint: Adjust the smoothstep edges.)

(Feel free to experiment directly in the editor above!)

Bonus Function: fract for Repetition

Often we want repeating patterns. The fract function is great for this. It simply returns the fractional part of a number (e.g., fract(3.7) is 0.7, fract(1.2) is 0.2).

If you multiply vUv.x or vUv.y by a number before applying fract, the pattern will repeat that many times across the surface.

Example: Simple Stripes

Challenge #3: Checkerboard Pattern

Combine horizontal and vertical stripes (using fract and step) to create a checkerboard pattern.

Hint: A common way to make a checkerboard is to get the step value for x and y (let's call them sx and sy, both being 0 or 1). A checkerboard pattern alternates when either sx or sy is 1, but not both. The XOR (exclusive OR) operation is perfect for this. In GLSL, you can often simulate XOR for 0/1 values like this: abs(sx - sy) or mod(sx + sy, 2.0).

Saved

Practical Project: Mondrian-Style Composition (Challenge #4)

Now for a slightly bigger challenge that combines these ideas. Let's create a composition inspired by Piet Mondrian's geometric art.

Exemple of Piet Mondrian's art

I first encountered this exercise in Wawa Sensei's fantastic Three.js Journey course, and honestly, it was a huge "aha!" moment for me. It really clicked how these simple step and mix functions could be layered to build something visually structured and interesting. It's a perfect exercise for understanding boundaries and layering colors.

The Goal: Use step functions to define rectangular areas and mix to "paint" them with Mondrian's signature colors (red, blue, yellow, white, black lines).

We'll use a helper function drawRectangle that returns 1.0 inside the defined rectangle and 0.0 outside. Your task is to call this function multiple times with different coordinates and colors, layering them using mix. Remember that black lines often separate the colored blocks.

Saved

Conclusion: Keep Experimenting!

So, we've played with step, mix, smoothstep, and fract. Even with just these basic functions, we can start creating divisions, gradients, simple shapes, and repeating patterns. The Mondrian challenge hopefully showed how layering these simple ideas can build up complexity.

My biggest takeaway so far? Just try stuff! Change values, swap vUv.x for vUv.y, combine functions in weird ways. Sometimes it breaks, sometimes it looks ugly, but sometimes you stumble onto something cool. That's how we learn, right?

In the next part of the series, maybe we'll look at manipulating time (uniform float u_time), using noise functions, or working with textures. Let me know what you'd be interested in exploring! Keep coding, keep experimenting, and let's continue learning shaders together.