Shortcuts ⌨️

  • SPACE to play / pause
  • ARROW RIGHT or L to go forward
  • ARROW LEFT or J to go backward
  • ARROW UP to increase volume
  • ARROW DOWN to decrease volume
  • F to toggle fullscreen
  • M to toggle mute
  • 0 to 9 to go to the corresponding part of the video
  • SHIFT + , to decrease playback speed
  • SHIFT + . or ; to increase playback speed

Unlock content 🔓

To get access to 91 hours of video, a members-only Discord server, subtitles, lesson resources, future updates and much more join us for only $95!

Want to learn more? 🤘


That's the end of the free part 😔

To get access to 91 hours of video, a members-only Discord server and future updates, join us for only $95!

Next lesson

Introduction 00:00

Let’s continue our particles series with a classic yet difficult exercise. We are going to morph particles:

There are multiple ways of doing it and we could do it in JS by updating the attribute itself. But as we saw in previous lessons, updating the attribute is bad for performance and it would be impossible to maintain a good frame rate with a large amount of particles.

Another solution is by using GPGPU. It’s a good, but difficult solution which is why we won’t use it. Although it’s not for particle morphing, we’ll learn about GPGPU in one of the next lessons.

Finally, there is an easier solution that relies mostly on the vertex shader, and that we are going to discover now.

Setup 01:39

The starter is quite similar to the previous lesson with particles ready to be enhanced:

  • A tweakable clearColor
  • Some models in one file named models.glb that you can find in the static/ folder (you can download the original Blender file using the Resources button)
  • A GLTFLoader instance with a DracoLoader instance associated with it
  • A sphere made out of particles using a ShaderMaterial
  • The vite-plugin-glsl dependency to handle GLSL files
  • OrbitControls to rotate around
  • gsap in the dependencies

The particle size is already handled in the vertex.glsl. We use a uSize uniform to control it from the JS. Then, perspective is applied and the final size is relative to the height of the render, as seen in previous lessons.

Also, note that everything related to particles has been set as properties of the particles object to keep things organised.

Theory 04:18

The idea is that we are going to send two sets of positions to the vertex shader, one being the initial shape, and another one being the targeted shape. For the initial shape, we can use the classic position attribute. For the targeted shape, we need to send a new attribute. Let’s call it aPositionTarget.

Then, we send a uniform which is going to be a float going from 0 to 1. Let’s name it uProgress. And we are going to use that uProgress to mix between position and aPositionTarget.

Transitioning from one shape to another should be as simple as animating the uProgress.

Obviously, we are going to face difficulties and add cool effects on top of it.

Pattern 06:06

Before tackling the morphing, let’s have some fun and draw a pattern instead of those squares.

We want to draw a bright point at the center of the particle and have that point fade out before it reaches the edges of the particle. We actually did that in the Shader Patterns lesson, but let’s create it from scratch:

In fragment.glsl, retrieve the particle UV using gl_PointCoord, save it as uv and send it to the gl_FragColor to make sure it’s working:

void main()
    vec2 uv = gl_PointCoord;

    gl_FragColor = vec4(uv, 1.0, 1.0);
    // ...

Next, we need the distance to the center. We are going to use the technique we saw in one of the previous lessons by using the length of the offset uv instead of the distance from vec2(0.5) and send it to gl_FragColor:

void main()
    vec2 uv = gl_PointCoord;
    float distanceToCenter = length(uv - 0.5);
    gl_FragColor = vec4(distanceToCenter, distanceToCenter, distanceToCenter, 1.0);
    // ...

Using distanceToCenter, we can calculate the alpha.

We want it to be very high at first and plunge very fast. We can use the small number division technique.

The idea is that we take a number below 1 and divide it by an increasing value (distanceToCenter in our case):

Create a float alpha variable, to which you assign 0.05 , divided by distanceToCenter. Send it to gl_FragColor:

void main()
    // ...
    float alpha = 0.05 / distanceToCenter;
    gl_FragColor = vec4(alpha, alpha, alpha, 1.0);
    // ...

Not bad, but alpha never reaches 0.0 at the edges.

Want to learn more?

That's the end of the free part 😔

To get access to 91 hours of video, a members-only Discord server and future updates, join us for only $95!

How to use it 🤔

  • Download the Starter pack or Final project
  • Unzip it
  • Open your terminal and go to the unzip folder
  • Run npm install to install dependencies
    (if your terminal warns you about vulnerabilities, ignore it)
  • Run npm run dev to launch the local server
    (project should open on your default browser automatically)
  • Start coding
  • The JS is located in src/script.js
  • The HTML is located in src/index.html
  • The CSS is located in src/style.css

If you get stuck and need help, join the members-only Discord server: