Wawa Sensei logo
🎥 New lesson, the video will be released in the coming days!

Shaping functions

Starter pack

The shader logic is special, it's different from what we are used to in JavaScript or other programming languages. While the syntax is similar, the way we implement logic is different. It's a bit like a black box, you put in some data and you get out some colors for the fragment shader, or some positions for the vertex shader.

But how do you control the output? How do you make it look like you want? This is where shaping functions come in.

We will focus on the fragment shader for this lesson, but the same principles apply to the vertex shader.

Practice zone

To master the art of shaping, you will need to practice and experiment. It will take time to get used to the way the shader works, but once you get the hang of it, you will be able to create amazing effects.

Because I want you to put you in the best position to succeed, I've prepare this 3D Art Gallery scene for you to visualize your shaders in real time in a pleasant environment.

Can you feel your creativity flowing? All those empty frames are waiting for your masterpieces!

The 3D model used is VR Gallery by Maxim Mavrichev and is licensed under Creative Commons Attribution.

I used <CameraControls /> to create a first person camera to navigate inside the gallery and Squoosh to reduce the size of the textures.

SimpleShaderMaterial

All the frames from the gallery are copies of the SimpleShaderMaterial, it is a basic custom shader I prepared for you with:

  • uColor: a color
  • uTime: the time elapsed since the beginning of the application
  • vUv: the UV coordinates of the fragment (0 to 1 on both axis)

They all are extended in the <App.jsx /> to be able to use them declaratively in our scene.

They are named based on their position in the gallery, but feel free to rename them to something more meaningful once you created masterpieces with them!

Functions

For each fragment of our shader, our same code will be executed with different inputs.

If we want to draw a line, we could use if statements to check if the pixel is inside the line or not. But doing more complex shapes would be very difficult and inefficient.

Instead, we use functions to shape our output. Do not worry, you don't need to be a math expert to use them. You just need to understand what functions are at your disposal, and what they do.

Think of a function as a machine that takes some input and gives you some output. For example, the function f(x) = x * 2 takes a number x and gives you x * 2 as a result.

To visualize the effect of the different functions, we will use Graphtoy, it's a simple tool to type in functions and see their output. It helps validate our understanding of the functions when we are experimenting with them in our shaders.

Let's visualize our f(x) = x * 2 function in Graphtoy:

Graphtoy

Graphtoy will quickly become your best friend when you are experimenting with shaders.

Time to experiment with the different functions at our disposal.

Step

The step function is a simple function that returns 0 if the input is less than a threshold, and 1 if the input is greater than the threshold.

It takes two parameters:

  • edge: the threshold
  • x: the input value

Let's try it on the front frame ArtFront02Material.jsx fragment shader:

void main() {
  float pct = 1.0;
  pct = step(0.5, vUv.x);
  vec3 finalColor = pct * uColor;
  gl_FragColor = vec4(finalColor, 1.0);
}

We declare a variable pct to determine the percentage of the color we want to display. We set it to 1.0 by default, and then we use the step function to set it to 0.0 if the vUv.x is less than 0.5.

Step function representation

We can see the left half of the frame is black, and the right half is the color set in the uColor uniform.

Let's apply it to the vertical axis with a different threshold:

void main() {
  float pct = 1.0;
  pct = step(0.2, vUv.y);
  vec3 finalColor = pct * uColor;
  gl_FragColor = vec4(finalColor, 1.0);
}

Step function vertical representation

We can see the 20% of the bottom of the frame is black, and the rest is purple.

The UV coordinates have the origin at the bottom left corner of the frame, so [0, 0] is the bottom left corner, and [1, 1] is the top right corner.

If you want to revert the effect, you can simply swap the parameters of the step function:

void main() {
  float pct = 1.0;
  pct = step(vUv.y, 0.2);
  vec3 finalColor = pct * uColor;
  gl_FragColor = vec4(finalColor, 1.0);
}

Step function vertical reverted representation

We now have the opposite effect.

Mix

The mix function is a simple function that returns a linear interpolation between two values.

It takes three parameters:

  • x: the first value
  • y: the second value
  • a: the value to interpolate between x and y

Let's try it:

void main() {
  float pct = 1.0;
  pct = mix(0.0, 1.0, vUv.x);
  vec3 finalColor = pct * uColor;
  gl_FragColor = vec4(finalColor, 1.0);
}

Mix function representation

We can see a nice gradient from black to purple.

As many other functions, you can use it on other types of data, like the vector components. Let's use it to create a gradient from white to purple on the vertical axis:

void main() {
  vec3 whiteColor = vec3(1.0);
  vec3 finalColor = mix(whiteColor, uColor, vUv.y);
  gl_FragColor = vec4(finalColor, 1.0);
}

Mix function two colors

We can see a nice gradient from white to purple.

Let's change the interpolation value by calling the mix function a first time to get the pct value, and then use it to interpolate between the whiteColor and the uColor:

void main() {
  vec3 whiteColor = vec3(1.0);
  float pct = mix(0.0, 0.3, vUv.y);
  vec3 finalColor = mix(whiteColor, uColor, pct);
  gl_FragColor = vec4(finalColor, 1.0);
}

Mix function two colors with pct

The purple is very light as the maximum value of pct is 0.3.

Use Graphtoy to experiment with the mix function and understand how it works.

Graphtoy mix function

3 different interpolations with the mix function give completely different results.

Smoothstep

The smoothstep function is a simple function that returns a smooth interpolation between two values.

It takes three parameters:

  • edge0: the lower edge
  • edge1: the upper edge
  • x: the value to interpolate between edge0 and edge1

It gives three different results:

  • 0.0 if x is less than edge0
  • 1.0 if x is greater than edge1
  • a smooth interpolation between 0.0 and 1.0 if x is between edge0 and edge1

Let's use it to change our pct value:

void main() {
  vec3 whiteColor = vec3(1.0);
  float pct = smoothstep(0.4, 0.6, vUv.y);
  vec3 finalColor = mix(whiteColor, uColor, pct);
  gl_FragColor = vec4(finalColor, 1.0);
}

Smoothstep function representation

The transition is now only between 0.4 and 0.6. Everything below is white, and everything above is purple.

Min & Max

min and max are simple functions that return the minimum or maximum value between two inputs. They work exactly like the Math.min and Math.max functions in JavaScript.

Let's use them to fine-tune our pct value. First let's make sure it's never below 0.4 (which means never totally white):

void main() {
  vec3 whiteColor = vec3(1.0);
  float pct = smoothstep(0.4, 0.6, vUv.y);
  pct = max(pct, 0.4);
  vec3 finalColor = mix(whiteColor, uColor, pct);
  gl_FragColor = vec4(finalColor, 1.0);
}

And never above 0.6 (which means never totally purple):

void main() {
  vec3 whiteColor = vec3(1.0);
  float pct = smoothstep(0.4, 0.6, vUv.y);
  pct = max(pct, 0.4);
  pct = min(pct, 0.6);
  vec3 finalColor = mix(whiteColor, uColor, pct);
  gl_FragColor = vec4(finalColor, 1.0);
}

Min & Max function representation

The colors are washed out, and the transition is very smooth.

Let's vizualize it on Graphtoy:

Max graph

max(x, 0.4) representation

Min graph

min(x, 0.6) representation

And if we combine them:

Min & Max graph

You can see our values never go below 0.4 and never go above 0.6.

Our combination of min and max functions can be replaced by the clamp function, which is a shorthand for min(max(x, min), max).

Operations & patterns

Before we discover many other useful functions, let's see how we can use operations to create patterns.

End of lesson preview

To get access to the entire lesson, you need to purchase the course.