Create live snow effect for your website
Tags: tutorial, webdev, javascript, programming
Published: 2022-07-10 15:58:32

Isn't it fascinating how Threejs can create websites that turn into guns and blow your mind off🤯? Wait, do you know Threejs?

## Sections
[Introduction](#introduction)
[Project structure](#project-structure)
[Getting started](#getting-started)
[Creating a scene](#creating-a-scene)
[Creating particles](#creating-particles)
[Animate the particles](#animate-the-particles)

### Introduction

Let's start here. [Threejs](https://threejs.org/) is a lightweight 3D library with a rich feature set that abstracts complexities of WebGL and makes it super simple to get started with 3D programming on the web. With that said, let's see if this example of 3D website will blow off your mind! https://bruno-simon.com

We are going to cover steps on how to create a particle system with Threejs. It is beginner friendly and requires bit of acquaintance with javascript.

You can see the final result [here](https://inquisitive-toffee-7575d9.netlify.app)
I implemented this in my portfolio. You should [visit it](https://lookupzach.netlify.app) if you wanna see a mindblowing website😜.

### Project structure

You can `git clone` the starterpack of our project structure and resources [here](https://github.com/hane-smitter/three-particles-starterpack), so that we can focus on the fun stuff.

If you prefer to follow every step, then let's get going by following the steps below:

- Create a directory called `three-particles`.
- Change into the newly created directory(`three-particles`).
- Create an `index.html` file.
- Create a `script.js` file in the folder structure: `./assets/js/`
- Create `textures` and `sprites` folder inside of assets folder in the structure of `./assets/textures/sprites`. This is where we will put our images of snowflakes. You can find these images in repository starter pack shared above.

### Getting started

In our `index.html`, we are going to load in the **Threejs** Library and a little css styles reset.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <title>Threejs particles</title>
    <style>
      body {
        margin: 0;
        background-color: #000;
        color: #fff;
        font-size: 13px;
        overscroll-behavior: none;
      }
    </style>
  </head>
  <body>
    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/three@0.142.0/build/three.module.js",
          "three/": "https://unpkg.com/three@0.142.0/"
        }
      }
    </script>
<script type="module" src="./assets/js/script.js"></script>
  </body>
</html>

```

We have simply included `Threejs` into our project via Content Delivery Network(CDN).
The `<style>` tags add basic styling to our web page to override default styling along with changing body background to dark.
We have used **HTML Import maps** on `<script type="importmap">`  to alias the path of our import to just the term `three`. I have an article explaining more about import maps [here](https://dev.to/smitterhane/import-external-javascript-libraries-with-just-html-no-npm-install--552o). We have also defined a key `three/` which will allow us to import other utilities we may require from other directory(_folder_) locations of Threejs.
Below the `<script type="importmap">`, we have included our script tag loading our `script.js` file. This is where we will write our code.

## Creating a scene

Let's head to our `script.js` file and start off.
I am going to **create a scene** that will contain our particles and simulate the particles to give a visual effect of a snow fall.
Below is the starting code:

```javascript
import * as THREE from "three";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";

let camera, scene, renderer, stats, parameters;
let mouse = { X: 0, Y: 0 };

let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;

const materials = [];

init();
animate();

function init() {
  camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    1,
    2000
  );
  camera.position.z = 1000;

  scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x000000, 0.0008);

  const particleSystems = createParticleSystems();

  // Add particleSystems to scene
  for (let i = 0; i < particleSystems.length; i++) {
    scene.add(particleSystems[i]);
  }

  // rendering output to the browser
  renderer = new THREE.WebGLRenderer();
  renderer.setPixelRatio = window.devicePixelRatio;
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  // Showing stats
  stats = new Stats();
  document.body.appendChild(stats.dom);

  // Modify the 3D Object rendered with GUI
  // We toggle the texture to see results
  // when texture(image) is added and not
  const gui = new GUI();
  const params = {
    texture: true,
  };

  gui.add(params, "texture").onChange(function (value) {
    for (let i = 0; i < materials.length; i++) {
      materials[i].map = value === true ? parameters[i][1] : null;
      materials[i].needsUpdate = true;
    }
  });
  gui.open();


  document.body.style.touchAction = "none";
  // Update pointer locations on the screen
  document.body.addEventListener("pointermove", onPointerMove);

  // update the dom element with change in viewport
  window.addEventListener("resize", onWindowResize);
}

function onWindowResize() {
  windowHalfX = window.innerWidth / 2;
  windowHalfY = window.innerHeight / 2;

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

function onPointerMove(event) {
  if (event.isPrimary === false) return;

  mouse.X = event.clientX - windowHalfX;
  mouse.Y = event.clientY - windowHalfY;
}
```

You may see function calls that are not defined. We shall get to them. Rather than splitting these functions into separate modules, in this tutorial I decided to use the hoisting behavior of Javascript functions.

The `windowHalfX` and `windowHalfY` variables are meant to map the browser coordinate system into the Threejs coordinate system. For instance in browser, the more left of the window you get to, the closer you get to the value of zero. In Threejs, the x-axis is 0 at the middle and negative leftwards and so, positive rightwards.

Think of Threejs 3D like a scene in movies. In the scene, you can add objects just like in a movie's scene; obstacles or buildings or cars...e.t.c can be added. Then there is a camera that will view the scene and it can be moved to different angles to get variations of **points of view**.

As you see here:

```javascript
camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    1,
    2000
  );
  camera.position.z = 1000;

  scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x000000, 0.0008);
```

We have created a camera, given it _an angle(Field Of View), aspect ratio, near and far_ parameters. We have set the `camera.position.z` to a value so that it draws the camera out on the `z-axis` which is the axis coming towards you from the screen.

This step is important so that our camera can be able to capture object(s) added to the scene in its field of view. By default, when we call `scene.add()`, the object(s) we add will be added to the coordinates (0,0,0). This would cause both the camera and the object(s) to be inside each other. So you will see nothing but all black.

We then created our scene, `scene = new THREE.Scene();` and added `scene.fog` to make object(s) appear faded as they move further away from the camera.
You can visit the [Threejs docs](https://threejs.org/docs/#manual/en/introduction/Creating-a-scene) to have a proper glimpse of the type of paramers taken by these methods.

### Creating Particles

Let's get to these line `const particleSystems = createParticleSystems();` in our `init()` function.
In the snippet below we add the definition of `createParticleSystems()` function:

```javascript
function createParticleSystems() {
  // Load the texture that will be used to display our snow
  const textureLoader = new THREE.TextureLoader();

  const sprite1 = textureLoader.load(
    "./assets/textures/sprites/snowflake1.png"
  );
  const sprite2 = textureLoader.load(
    "./assets/textures/sprites/snowflake2.png"
  );
  const sprite3 = textureLoader.load(
    "./assets/textures/sprites/snowflake3.png"
  );
  const sprite4 = textureLoader.load(
    "./assets/textures/sprites/snowflake4.png"
  );
  const sprite5 = textureLoader.load(
    "./assets/textures/sprites/snowflake5.png"
  );

  // Create the geometry that will hold all our vertices
  const geometry = new THREE.BufferGeometry();
  const vertices = [];
  const particleSystems = [];

  // create the vertices and add store them in our vertices array
  for (let i = 0; i < 10000; i++) {
    const x = Math.random() * 2000 - 1000; // generate random number between -1000 to 1000
    const y = Math.random() * 2000 - 1000;
    const z = Math.random() * 2000 - 1000;

    vertices.push(x, y, z);
  }

  // Add the vertices stored in our array to set
  // the position attribute of our geometry.
  // Position attribute will be read by threejs
  geometry.setAttribute(
    "position",
    new THREE.Float32BufferAttribute(vertices, 3)
  );

  parameters = [
    [[1.0, 0.2, 0.5], sprite2, 20],
    [[0.95, 0.2, 0.5], sprite3, 15],
    [[0.9, 0.2, 0.5], sprite1, 10],
    [[0.85, 0.2, 0.5], sprite5, 8],
    [[0.8, 0.2, 0.5], sprite4, 5],
  ];

  for (let i = 0; i < parameters.length; i++) {
    const color = parameters[i][0];
    const sprite = parameters[i][1];
    const size = parameters[i][2];

    // Create the material that will be used to render each vertex of our geometry
    materials[i] = new THREE.PointsMaterial({
      size,
      map: sprite,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      transparent: true,
    });
    materials[i].color.setHSL(color[0], color[1], color[2]);

    // Create the particle system
    const particleSystem = new THREE.Points(geometry, materials[i]);

    /* Offset the particle system x, y, z to different random points to break
    uniformity in the direction of movement during animation */
    particleSystem.rotation.x = Math.random() * 6;
    particleSystem.rotation.y = Math.random() * 6;
    particleSystem.rotation.z = Math.random() * 6;

    particleSystems.push(particleSystem);
  }
  return particleSystems;
}
```

Here `const textureLoader = new THREE.TextureLoader();
` we instantiate a `THREE.TextureLoader()` class that we then use to load our images of snow.Three js will extract the texture from our images and map it to the texture of the particles we create.

We later ceate a geometry, `const geometry = new THREE.BufferGeometry();` that will be used to hold our vertices in groups of x, y, z. _That is, x,y,z represents one particle_. Particles are just individual vertices in a geometry.

```javascript
for (let i = 0; i < 10000; i++) {
    const x = Math.random() * 2000 - 1000; // generate random number between -1000 to 1000
    const y = Math.random() * 2000 - 1000;
    const z = Math.random() * 2000 - 1000;

    vertices.push(x, y, z);
  }
```

As shown above, we are creating vertices in the group (x, y, z) and adding them to our `vertices` array.
We then set a position attribute on our `geometry` and add our vertices to it. Threejs will interpret this attribute as the placement(positions) of our vertices on our geometry spread across x and y axes.

```javascript
for (let i = 0; i < parameters.length; i++) {
    const color = parameters[i][0];
    const sprite = parameters[i][1];
    const size = parameters[i][2];

    // Create the material that will be used to render each vertex of our geometry
    materials[i] = new THREE.PointsMaterial({
      size,
      map: sprite,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      transparent: true,
    });
    materials[i].color.setHSL(color[0], color[1], color[2]);

    // Create the particle system
    const particleSystem = new THREE.Points(geometry, materials[i]);

    /* Offset the particle system x, y, z to different random points to break
    uniformity in the direction of movement during animation */
    particleSystem.rotation.x = Math.random() * 6;
    particleSystem.rotation.y = Math.random() * 6;
    particleSystem.rotation.z = Math.random() * 6;

    particleSystems.push(particleSystem);
  }
  return particleSystems;
}
```

With our parameters defined in parameters array:

```javascript
// Create the material that will be used to render each vertex of our geometry
    materials[i] = new THREE.PointsMaterial({
      size,
      map: sprite,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      transparent: true,
    });
    materials[i].color.setHSL(color[0], color[1], color[2]);
```
We are now defining how our particles would look like(The material that would be added to our vertices, to enable us to give the vertices texture of our image and also color them). __Remember we said a single vertex x, y, z represents one particle__. The option `map` would map our particle to our png image texture. `transparent: true` rids the background of our png. Without it, we would see the background of our image creating weird view. `materials[i].color.setHSL(color[0], color[1], color[2]);` gives color to our material.

`const particleSystem = new THREE.Points(geometry, materials[i]);` creates the particles managed as a system.
`particleSystems.push(particleSystem);`, here we create several particleSystems and store to our array. We return `particleSystems` array containing all the created particle systems.

**Going back to our `init()` function**, below after calling `createParticleSystems()`, you can see we are iterating over the returned value and add each particle system to our scene. That is:

```javascript
const particleSystems = createParticleSytems();

// Add particleSystems to scene
  for (let i = 0; i < particleSystems.length; i++) {
    scene.add(particleSystems[i]);
  }
```
And lastly, from our `init` function, we render what we have created to the browser so that we can see:

```javascript
// rendering output to the browser
  renderer = new THREE.WebGLRenderer();
  renderer.setPixelRatio = window.devicePixelRatio;
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);
```

### Animate the particles
To animate the particles, we are going to change the orientation of the whole particle system. Our scene has 5 particle Systems(_length of our `parameters` array_) each with 10,000 particles.
We define our animate function:

```javascript
function animate() {
// This will create a loop rendering at each frame
  requestAnimationFrame(animate);
  render();
  stats.update();
}
function render() {
  const time = Date.now() * 0.00005;

  camera.position.x += (mouse.X - camera.position.x) * 0.05;
  camera.position.y += (mouse.Y - camera.position.y) * 0.05;

  camera.lookAt(scene.position);

  for (let i = 0; i < scene.children.length; i++) {
    const object = scene.children[i];

    if (object instanceof THREE.Points) {
      object.rotation.y = time * (i < 4 ? i + 1 : -(i + 1));
    }
  }

  for (let i = 0; i < materials.length; i++) {
    const color = parameters[i][0];
    const h = (360 * ((color[0] + time) % 360)) / 360;
    materials[i].color.setHSL(h, color[1], color[2]);
  }

  renderer.render(scene, camera);
}
```

in our `render` function, we are animating the position of our camera to move to where our mouse is currently at. But by multiplying the difference between the position of the camera and the position of the mouse by `0.05` will make the camera move slowly slowly drawing towards the mouse at every frame.

```javascript
for (let i = 0; i < scene.children.length; i++) {
    const object = scene.children[i];

    if (object instanceof THREE.Points) {
      object.rotation.y = time * (i < 4 ? i + 1 : -(i + 1));
    }
  }
```

In this block of code, we are animating our particleSystems around the Y-axis. You can see that in every frame we are changing y value `object.rotation.y = time * (i < 4 ? i + 1 : -(i + 1));`. We are using time, to ensure that our particleSystems rotate at the same speed regardless of the device which may have different frame rates. Though browsers in most devices have a Frame Rate of _60 frames per second(60FPS)_. Remember that time variable we defined as `const time = Date.now() * 0.00005;` would increment at every frame and we multiply by a small factor `0.00005` to make small changes in the rotation around the y axis.

And that is how we created our particles and simulated it to give a snowfall effect.
The final code of our tutorial is in the [repo](https://github.com/hane-smitter/threejs-particles).
I'll see you in my next article✌️.

_[Follow me on twitter](https://twitter.com/smitterhane) to see what am upto these days. I share tips and tricks and hacky work arounds about javascript programming._