How I created my first three.js project

How I created my first three.js project

Jupiter and Saturn conjunction

Photo by [Zoltan Tasi](https://cdn.hashnode.com/res/hashnode/image/upload/v1624386303716/JhGwMjeJ_.html) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)Photo by Zoltan Tasi on Unsplash

I divided this article to several parts:

  • Idea

  • Three.js basics

  • Create the stars (universe)

  • Add the planets

  • Add music

  • Loop animation

**Demo / Code on Github: [miresk/planets-conjunction** You can’t perform that action at this time. You signed in with another tab or window. You signed out in another tab or…*github.com](https://github.com/miresk/planets-conjunction/blob/master/main.js)

*I recommend opening the demo on bigger screens (the flaws of the app are listed at the end of article)

Idea

A few years ago I discovered this Christmas experiments website — an advent WebGL calendar featuring various digital artists. It looks like the project has been abandoned and the last version is from 2018.

Anyway, my mind was blown away when I checked the examples and I felt inspired. But learning three.js, canvas or WebGL also felt like a daunting task and my aspirations faded away as quickly as they emerged. I’ve got an idea for a simple project in late 2020 thanks to a friend of mine.

I was thinking first about creating something Christmassy, involving snow, trees etc. but eventually I found out that Jupiter and Saturn are closing in on their great conjunction in December 21, 2020. The last time this happened was 400 years ago. As npr.org writes:

Seen at the right hour, whether by telescope or the naked eye, the gas giants will be separated by roughly a fifth of the diameter of the typical full moon. At this proximity, the planets will appear to touch or even form one large, brilliant star in the sky.

This is a big, exciting event and I decided to visualize it using web technologies. There are many three.js examples online that helped me to put it together. This is a preview of the result:

Saturn and Jupiter conjunctionSaturn and Jupiter conjunction

I will describe only briefly a few concepts of three.js and specific parts of the project so for more implementation details I recommend this free book — Discover three.js that explains everything you need to know to get started!

Three.js basics

Three.js consists of 3 main parts:

  • Scene

  • Camera

  • Renderer

**Scene* is the universe you can see in the Gif above. It’s a 3D space with a coordinate system that has 3 axes — x, y, z *(0,0,0 is the center of that universe). We create it like this:

scene = new THREE.Scene();

There are several types of cameras, but the common one is **Perspective camera**. It mimics the way we see. There is a thing called frustum, which is part of the space that appear on the screen.

[Viewing frustum](https://cdn.hashnode.com/res/hashnode/image/upload/v1624386309234/Qsobjw5ML1.html)Viewing frustum

For example some things positioned behind it wouldn’t be visible. The camera is defined like this:

camera = new THREE.PerspectiveCamera(
  fieldOfView,
  aspectRatio,
  nearPlane,
  farPlane
);

camera.position.x = 0;
camera.position.y = 100;
camera.position.z = 200;

Renderer displays the scene on your screen (canvas) using WebGL library. It’s called WebGLRenderer and is defined as:

renderer = new THREE.WebGLRenderer();

As a final step we have to call the render() method in which we pass the previously created scene and camera.

renderer.render(scene, camera);

At this point we would not see anything yet as we didn’t create any geometric objects nor lights to illuminate them.

Fun fact: Everything you can see in the scene is made of triangles.

S[pheres](https://cdn.hashnode.com/res/hashnode/image/upload/v1624386310633/KJz3tYgJh.html) are triangles too :-)Spheres are triangles too :-)

Pretty much everything else also derives from the main THREE class so for the sphere above we could write:

*const* sphereGeometry = new THREE.SphereGeometry(1, 32, 16);

I wanted to explain basics of three.js first before we get more specific.

Create the stars (universe)

We can create the stars with the geometry (BufferGeometry) and particles (Points).

BufferGeometry — A representation of mesh, line, or point geometry. Includes vertex positions, face indices, normals, colors, UVs, and custom attributes within buffers, reducing the cost of passing all this data to the GPU.

You can imagine it as a rectangle with many points inside.

[The star particles](https://cdn.hashnode.com/res/hashnode/image/upload/v1624386312369/u3MOdquKy.html)The star particles

Below in the code you can see that we define a geometry variable and an array of vertices. We create a loop for 6 thousand stars, and we fill the vertices array with randomly distributed values (x, y, z). You can imagine the rectangle above filled with 6000 points.

We pass 1500 as a value for each coordinate. Play with the numbers to define the boundaries. We then set the position of these values using geometry.setAttribute method.

*const* geometry = new THREE.BufferGeometry();
*const* vertices = [];

for (let i = 0; i < 6000; i++) {
  vertices.push(THREE.MathUtils.randFloatSpread(1500)); // x
  vertices.push(THREE.MathUtils.randFloatSpread(1500)); // y
  vertices.push(THREE.MathUtils.randFloatSpread(1500)); // z
}

geometry.setAttribute(
  "position",
  new THREE.Float32BufferAttribute(vertices, 3)
);

Now let’s create the particles that would represent the Stars.

particles = new THREE.Points(
  geometry,
  new THREE.PointsMaterial({ color: 0x888888 })
);

scene.add(particles);

We could add them some nice texture, but it’s sufficient to define just a color in this case. Here’s the result of our static universe. We will animate it later.

Add the planets

To create a planet we have to use *THREE.SphereGeometry()*.

We also use TextureLoader() to add textures (images) to the spheres so they look like the planets we want to use. The textures are then mapped to the material. The main realistically looking material in three.js is *MeshStandardMaterial. With this material, we have to create some lights (eg. [PointLight](threejs.org/docs/?q=PointLight#api/en/light..) and add them to the scene in order to see the objects (planets).

In the code below we create three.js Mesh object for each planet that takes 2 parameters defined before — geometry and material.

There is a helper function createPlanet() we use to make the code more concise (DRY).

*const* sphereGeometry = new THREE.SphereGeometry(1, 32, 16);

// TEXTURES
const loader = new THREE.TextureLoader();
const sunTexture = loader.load("assets/sun.jpg");
const jupiterTexture = loader.load("assets/jupiter.jpg");
const saturnTexture = loader.load("assets/saturn.jpg");

// MATERIALS
const sunMaterial = new THREE.MeshStandardMaterial({ map: sunTexture });
const jupiterMaterial = new THREE.MeshStandardMaterial({map: jupiterTexture,});
const saturnMaterial = new THREE.MeshStandardMaterial({ map: saturnTexture });

// SUN
sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
sunMesh.position.set(0, -200, 0);
sunMesh.scale.setScalar(200);
scene.add(sunMesh);

// JUPITER
jupiterGroup = new THREE.Group();
var jupiterGroupRadius = -300;
var jupiterGroupX = (2 * Math.PI * jupiterGroupRadius) / 4;
jupiterMesh = new THREE.Mesh(sphereGeometry, jupiterMaterial);
createPlanet(scene, jupiterMesh, jupiterGroup, jupiterGroupX , 23, jupiterGroupRadius, 20);

// SATURN
saturnGroup = new THREE.Group();
var saturnGroupRadius = -400;
var saturnGroupX = (2 * Math.PI * saturnGroupRadius) / 4;
saturnMesh = new THREE.Mesh(sphereGeometry, saturnMaterial);
createPlanet(scene, saturnMesh, saturnGroup, saturnGroupX, 10, saturnGroupRadius, 18);

// LIGHT
const light = new THREE.PointLight("white", 1.2);
light.position.set(0, 0, 0);
scene.add(light);

function createPlanet(scene, mesh, group, x, y, z, scale) {
  mesh.position.set(x, y, z);
  mesh.scale.setScalar(scale);
  group.add(mesh);
  scene.add(group);
}

I wanted the top part of the Sun to appear in the middle of the screen, so I scaled it up by 200 and moved it on the Y-axis by -200. That’s because the camera is already moved on the Y and Z-axes.

In Three.js the angles are specified in radians, so instead of 360∘ in a circle, there are 2π radians. To position the planets in the scene I used some basic trigonometry.

Add music

Before we get things into motion, let’s add the music to the scene first. I added a ‘Play’ button that starts the music and animation. To make things work, I just copied and adjusted a snippet from the official documentation about Audio.

*function* playScene() {
  *var* music = document.getElementById("music");
  music.classList.add("playing");
  playBtn.disabled = true;

  // create an AudioListener and add it to the camera
  const listener = new THREE.AudioListener();
  camera.add(listener);

  // create a global audio source
  const sound = new THREE.Audio(listener);

  // load a sound and set it as the Audio object's buffer
  const audioLoader = new THREE.AudioLoader();
  audioLoader.load("assets/Orloe-PassingJupiter.mp3", function (buffer) {
    sound.setBuffer(buffer);
    sound.setLoop(false);
    sound.setVolume(0.5);
    dur = buffer.duration.toFixed(0); // 176s
    sound.play();
    rotatePlanets();
  });
}

A track I used is called Passing Jupiter from Orloe and you can see also that I store a duration of the track. The duration is used in the animation loop to determine how long it should take for planets to rotate till they get into middle of the screen and form the new star.

Loop animation

In three.js we can animate any object properties. For example we can change rotation, scaling or colours of the objects over time. There are many examples online using *requestAnimationFrame() Javascript method, but newer versions of Three.js provide us with the .setAnimationLoop method, which internally uses requestAnimationFrame*.

*// start the loop*
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});

Let’s rotate the planets inside this method. We also have to handle time across frames though. For that we can use the Three.js Clock class and its method getDelta(), which tells us how much time has passed from the previous frame.

The full function that I use to rotate the planets looks like this:

*function* rotatePlanets() {
  clock.start();

  renderer.setAnimationLoop(() => {
    const time = clock.elapsedTime;
    const delta = clock.getDelta();

    sunMesh.rotation.y += 0.03 * delta;
    particles.rotation.y += 0.01 * delta;

    if (time <= (dur/2)) {
      jupiterGroup.rotation.y -= delta/(dur/2);
      saturnGroup.rotation.y -= delta/(dur/2);
      jupiterMesh.rotation.y += 0.015 * delta;
      saturnMesh.rotation.y += 0.025 * delta;
    } else if (time > (dur/2)) {
      scene.add(flareLight);
      renderer.setAnimationLoop(null);
    }

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

Bruno Simon in his course uses *getElapsedTime method instead as he says we shouldn’t use getDelta*:

Another available method is **getDelta(...)**, but you should not use it unless you know exactly what's going on in the Clock class code. Using it might mess with your animation, and you'll get unwanted results.

Well, I don’t know what’s going on exactly in the Clock but getDelta() works for me now. Nevertheless it’s good to know we have more options here.

Summary

I created this small project to try and learn some parts of Three.js. I explained briefly how the basics of three.js work and then I described specific parts of the projects providing the code snippets. It’s not a step by step detailed explanation but for a reference I listed a few good resources to learn Three.js in detail.

There’s a lot of space for improvement in the app. I’d like to load resources properly and display some nice progress bar before everything is fully loaded. Now we can see a blank screen for a second as I just use setTimeout before rendering the scene (not good).

I wanted the planets to move from the left part of the screen to the center where they overlap. I was fiddling with the values to get the result I was happy with but on mobile screens we can’t see the planets for several seconds. Therefore responsive behavior and calculations could be improved.

Three.js is very cool technology for creating immersive 3D web experiences and I am glad I could put this imperfect demo together and learned something new.

Give it a try! You are welcome to share your attempts :)

**Demo**

Code on Github: miresk/planets-conjunction You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…github.com