Getting Started With Your First three.js Project: Part Two — The Build

Daniel Waldow
Nerd For Tech
Published in
18 min readApr 12, 2021
Press ‘Run Pen’ to see a live demo of what we will be building

Welcome Back!

This is the second part of a two part tutorial on getting started with three.js. If you haven’t seen part one, you can check it out here to get setup, then come back to finish the build.

To recap, we should have the following code so far:

index.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, initial-scale=1.0">
<title>My First three.js Project</title>
<link rel="stylesheet" href="css/main.css">
</head>
<body>
// This line imports our javascript code from our /js directory
<script type="module" src="./js/main.js"></script>
</body>
</html>

main.css

body {
margin: 0px;
height: 100vh;
}
canvas {
display: block;
}

main.js

import * as THREE from '../node_modules/three/build/three.module.js';
import { TrackballControls } from '../node_modules/three/examples/jsm/controls/TrackballControls.js';
// Scene
const scene = new THREE.Scene();
// Camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.6, 1200);
// Renderer
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setClearColor("#233143");
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Make Canvas Responsive
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
})

If you get stuck at any point, you can see the final code solution by clicking the ‘JS’ button after running the pen above.

Building the Cube

Now that we have our environment completely setup with a scene, a camera and a renderer to draw everything for us, it is time to build our first object!

The best place to start in our scene would be to add our centrepiece, the box! To do this we can declare a few constants with the following three.js syntax:

// Create Box
const boxGeometry = new THREE.BoxGeometry(2, 2, 2);
const boxMaterial = new THREE.MeshLambertMaterial({color: 0xFFFFFF});
const boxMesh = new THREE.Mesh(boxGeometry,
boxMaterial);
boxMesh.rotation.set(40, 0, 40);
scene.add(boxMesh);
  1. In our first line, we declare our constant boxGeometry which represents the skeleton for our box, then call the new keyword with the .BoxGeometry function, passing in the width, height and depth as arguments
  2. Secondly, we declare the material we want to use for our box. There are many different types of materials to choose from, but for our example we will be using .MeshLambertMaterial, giving it a white colour by passing in an options object (this will become important for later, but you could really choose any colour you want)
  3. We put these two constants to use in our third line, by creating a new .Mesh that builds a box based on the geometry and material we just defined
  4. We can optionally change the initial rotation of our box as seen on line four with our .rotation.set functions, passing in a number (float) for the x, y and z rotation in degrees
  5. Finally, we add our box to the scene with the .add function with our newly created box as our argument

A step back in time…

I hope you remember your high school coordinate geometry, because it will be a huge help for the remainder of our tutorial. Who said you would never use high school mathematics ever again?

Source: brilliant.org

The final step above will add our cube at the origin (0, 0, 0) by default just like our camera (which is currently also sitting at the origin), meaning our camera will be inside our box and we won’t see anything!

To solve this, we can offset our camera to a different set of coordinates by changing the initial position. To do this we can add the following line under our camera section:

// Camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.6, 1200);
camera.position.z = 5; // <- New code

If you’ve followed every step of the way so far, well done! You are probably thinking, however: “We’ve setup our environment; with a scene, camera and renderer, we’ve made our canvas responsive AND we’ve built and added our cube to the scene. SO WHY CAN’T I SEE ANYTHING?!”

Rendering the Scene

We can’t see anything yet because we haven’t rendered anything yet. We have our renderer ready to go, but haven’t provided it with any instructions!

Let’s go ahead and do that now:

const rendering = function() {
requestAnimationFrame(rendering);
// Constantly rotate box
scene.rotation.z -= 0.005;
scene.rotation.x -= 0.01;
renderer.render(scene, camera);
}
rendering();

Our first line defines our render function which acts as an animation loop. What this loop will do is redraw our scene every time the screen refreshes (typically 60 times per second, also known as ‘Frames Per Second’ (FPS)).

It is important that this rendering function is at the bottom of your JavaScript file as it calls a number constants that need to be defined first (i.e. above the function that is referring to them).

Inside our block we have the following four lines:

  • requestAnimationFrame(rendering): This line creates the loop portion of the function, calling itself after every refresh (see how we’re passing the parent rendering function back in as an argument?). We could of course use setInterval() here instead, but the requestAnimationFrame() function has a number of distinct advantages as it is specifically designed for this type of rendering. Most importantly, the function stops looping when it recognises a user navigating to another tab, saving precious processing power
  • scene.rotation (.x and .z): To spruce up our box animation we can set a rotation value that the cube will update on every refresh, making the box look like it is spinning on its own
  • renderer.render(scene, camera): This line simply instructs our renderer constant to render our scene using our camera
Our first glimpse at the scene!

We should now be able to see our cube in our scene!!!

For those with a keen eye, it might be confusing why we are seeing a black box, when we set the colour of our box to the hex value for white (0xFFFFFF on our boxMaterial line). That is because we are looking at our box with the lights off…

Lighting

If we want to view our scene properly, we will need to turn the lights on.

“But, we don’t have any lights yet”.

Correct! Let’s add them now.

There are many types of lights to choose from, but for our example we will be adding PointLights. Their behaviour is most similar to a bare lightbulb in the real world, so they are the simplest to work with.

The following lines will add a light to your scene:

const light = new THREE.PointLight(0xFFFFFF, 1, 100);
light.position.set(5, 5, 5);
scene.add(light);

Let’s take a closer look at what is going on above:

In our first line, we declare a new light constant, using the PointLight constructor, which takes up to four parameters:

  1. Colour: This takes a hexadecimal value (like our white example earlier, 0xFFFFFF) to change the colour of the light
  2. Intensity: This takes a number, representing the intensity of the light
  3. Distance: Another number, defining the maximum range the light will travel from your light towards the scene (if this number is less than the distance to the objects in your scene the light obviously won’t reach the scene)
  4. Decay: The amount the light dims on its journey from the light to its maximum distance

In our example above, we only include three arguments, which just omits a value for decay, setting it to 1 by default.

Our second line sets the position of our light (in our example, the light is set five units away from the origin in the x, y and z planes).

Finally, our third line adds the light to our scene.

In our final build, users will be able to spin the cube in all directions, so if we only add one light, only one side of the cube can be seen, while the rest is dark. To solve this, we can add more lights.

We could write the three lines above several times over, changing the colour and position for each manually, but that produces a lot of repetitive code. A better way to do this is to store the unique light values in an array of objects and create a light constructor loop. One way of implementing this is written below:

// Lights
const lights = [];
const lightValues = [
{colour: 0x14D14A, intensity: 8, dist: 12, x: 1, y: 0, z: 8},
{colour: 0xBE61CF, intensity: 6, dist: 12, x: -2, y: 1, z: -10},
{colour: 0x00FFFF, intensity: 3, dist: 10, x: 0, y: 10, z: 1},
{colour: 0x00FF00, intensity: 6, dist: 12, x: 0, y: -10, z: -1},
{colour: 0x16A7F5, intensity: 6, dist: 12, x: 10, y: 3, z: 0},
{colour: 0x90F615, intensity: 6, dist: 12, x: -10, y: -1, z: 0}
];
for (let i=0; i<6; i++) {
lights[i] = new THREE.PointLight(
lightValues[i]['colour'],
lightValues[i]['intensity'],
lightValues[i]['dist']);
lights[i].position.set(
lightValues[i]['x'],
lightValues[i]['y'],
lightValues[i]['z']);
scene.add(lights[i]);
}

Firstly, we create an empty lights array (to store our lights in once we create them).

Next, we create a lightValues array where we store the values we want to use for each light’s colour, intensity, distance and position coordinates in an object (some tips for figuring these values out for your own scene can be found under the heading Controls and Helpers below). This makes it easier to reference the exact property we want to pass as an argument to our light constructors.

Finally, we create a simple for loop. In our example above, we are setting our i variable to 0 and incrementing up to (but not including) 6. This will give us 6 iterations, each with an i value that corresponds to the index of each object in our array above (e.g. i = 0, then 1, then 2 … up to 5; the 6th index of our objects).

In this loop we can inject a modified version of the original three lines of code we saw earlier:

  1. lights[i]: This syntax is a way of accessing an array element at a specific index, or in our case, creating a new pointLight object at the index defined by i in our current loop iteration. For our three arguments that we are passing in, we just need the i index of the lightValues array, then we can access the values we stored earlier in each object (e.g. lightValues[i][‘dist’] for the distance)
  2. lights[i].position.set: Now that we have created a light at i index for our current iteration, we can use the same trick again to set the x, y and z coordinates for the light position
  3. scene.add(lights[i]): Finally, we add the light of our current iteration to our scene
Looking better already!

Our cube is starting to look a lot better!

“But, it’s still not white!”

The reason our cube isn’t white is because we are shining coloured lights on a white cube!

The material we chose for our box has the properties of a non-shiny object that reflects light (which is why we needed lights to show our box properly).

I originally only used coloured lights to make it easier to tell them apart when using the three.js light helpers (more about these in Controls and Helpers below), but loved the way they looked on a white box so much (instead of a coloured box with white lights) I decided to keep them. If we take a closer look at the lightValues array in our code, we can see the hex values for each light are a specific set of colours (ones that I chose for the colour scheme of my portfolio website). I would encourage you to replace these with your favourite colours, which can be done easily with a colour picker (google has a good one here, the hex value is listed below the selected colour).

Controls and Helpers

The cube is looking pretty good so far. It is colourful and it spins on its own. What if we wanted to take control and spin it in the direction we wanted instead?

Trackball Controls

There are many types of controls we could use in our animation, but for our purposes, we want a camera control that will let us spin our box in any direction. Enter trackball controls:

//Trackball Controls for Camera 
const controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 4;
controls.dynamicDampingFactor = 0.15;

In our first line, we declare our controls constant, constructing a new TrackballControls object which takes two arguments:

  1. camera: The camera in our rendered scene
  2. domElement: The HTML element that will be used for event listeners (the objects ‘listening’ for any interactions from our user). In our case, we want the .domElement that our renderer is attached to

Our second line defines the speed of rotation of our scene when a user clicks and drags their mouse.

Finally, we set the .dynamicDampingFactor, which is a value used to determine how quickly the rotation will slow down once a user lets go of the mouse. Think of this like friction applied to the box spin when a user stops using the trackball controls.

BUT WAIT, THERE’S MORE!

What happens if we leave it here for the controls? They will work on the first frame of animation, but will never be updated again (meaning they will work for the first 1/60th of a second).

To solve this we need to add a line of code INSIDE our rendering function, to update our controls after each page refresh:

// Update trackball controls
controls.update();
Source: memegenerator.net

Now we can spin our box to our heart’s content!

Just click and drag your mouse (or press and flick on mobile) to send the cube flying!

Light Helpers

When I first started adding lights, it was a total pain to position them and set their properties correctly. This was made significantly easier after I stumbled across light helpers.

Light helpers are simple objects that you can add to your scene to properly visualise where your lights are in space and what their colour is set to (a useful trick for distinguishing between multiple lights).

The code to add these to your scene looks like this (new code in bold):

// Lights
const lights = [];
const lightHelpers = []; // <- New code
const lightValues = [
{colour: 0x14D14A, intensity: 8, dist: 12, x: 1, y: 0, z: 8},
{colour: 0xBE61CF, intensity: 6, dist: 12, x: -2, y: 1, z: -10},
{colour: 0x00FFFF, intensity: 3, dist: 10, x: 0, y: 10, z: 1},
{colour: 0x00FF00, intensity: 6, dist: 12, x: 0, y: -10, z: -1},
{colour: 0x16A7F5, intensity: 6, dist: 12, x: 10, y: 3, z: 0},
{colour: 0x90F615, intensity: 6, dist: 12, x: -10, y: -1, z: 0}
];
for (let i=0; i<6; i++) {
lights[i] = new THREE.PointLight(lightValues[i]['colour'], lightValues[i]['intensity'], lightValues[i]['dist']);
lights[i].position.set(lightValues[i]['x'], lightValues[i]['y'], lightValues[i]['z']);
scene.add(lights[i]);
// New Code: Add light helpers for each light
lightHelpers[i] = new THREE.PointLightHelper(lights[i], 0.7);
scene.add(lightHelpers[i]);
}

Just like our steps before on adding lights, we start by declaring a new empty array, lightHelpers.

In our new code at the bottom of the snippet (INSIDE the for loop), we use the same array accessing syntax to create a new PointLightHelper for each of our lights, passing in the light we want a helper for (e.g. lights[0] when i = 0), and a size for the helper (e.g. 0.7).

Finally, we add each helper to our scene.

Our rendered scene, with a coloured diamond ‘lightHelper’ for each light

Now it is much easier to see exactly where each light is in space, making the adjustment of each light’s x, y and z coordinates much easier. When you have multiple lights, it is also a good idea to change the colour of each light so it is easier to determine which light is being moved or edited.

Don’t forget to delete these lines (or comment them out with // ) when you are done with the light helpers.

Axes Helper

There are helpers for many different types of objects in three.js. Another really useful one is the Axes Helper. Let’s take a look:

// Axes Helper
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper); // X == red, Y == green, Z == blue

Here, we are declaring the axesHelper and constructing it by passing in a number for the length of the axis we want drawn.

Another helper, great for visualising the axes

We then add this to our scene. As noted in the code comment above, the X axis is red, the Y axis is green and the Z axis is blue.

This will become especially helpful later when we start tracing out orbital paths for our sphere objects. Again, when you are done with the axes helper don’t forget to delete these lines (or comment them out with // ).

Other Objects

Creating Our Orbiting Spheres

If you recall (or if you take a quick peek at the pen at the top of the page), you’ll remember we had four spheres orbiting the box in our animation. Let’s go ahead and create the spheres below:

// Create spheres: 
const sphereMeshes = [];
const sphereGeometry = new THREE.SphereGeometry(0.1, 32, 32);
const sphereMaterial = new THREE.MeshLambertMaterial({color: 0xC56CEF});
for (let i=0; i<4; i++) {
sphereMeshes[i] = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphereMeshes[i].position.set(0, 0, 0);
scene.add(sphereMeshes[i]);
}

You will probably notice a lot of similarity here. Firstly, have used the same logic seen in our light construction to create multiple objects with a loop (for brevity I won’t walk through each line in detail again here). Additionally, the constructor for a sphere has a lot in common with the box constructor we saw earlier, with a few minor differences.

There are a number of parameters our .SphereGeometry function can take, in our case we use the first three, which are:

  1. radius: The radius of the sphere as a float (decimal). Default is 1
  2. widthSegments: The number (integer) of horizontal segments
  3. heightSegments: The number (integer) of vertical segments

The larger the number used for width and height segments (to a maximum of 32) the ‘rounder’ the sphere will look.

We are using the .MeshLambertMaterial again for visual consistency. The only difference here is setting the default colour to a shade of purple instead of white. Feel free to change this back to white if the spheres look too dark to you.

Another quirk you may have noticed is that we set the position of every sphere to the origin. At this point you might be thinking “why would we place them all at the origin, won’t that put them all inside our box where we can’t see them?”

You will be pleased to know this initial position won’t matter, as we will be changing the position of the spheres in every refresh, so where it starts out is irrelevant.

Trigonometry Revisited

Apologies in advance for triggering any painful high school memories as we go back into the mathematics room for some ‘light’ trigonometry.

When I started this project I really wanted to have multiple spheres orbiting a box, similar to how one might model the electrons whizzing around an atom. This brings with it a few challenges:

  • The radius of each sphere needs to be different to ensure spheres don’t appear to be passing through each other in a collision (not that difficult)
Cartesian (left) and Polar (right) Coordinates, Source: wyzant
  • An orbit (typically measured in Polar Coordinates) for each sphere needs to be converted to Cartesian Coordinates to be understood by three.js (moderately difficult)
  • To model an orbital path for each sphere in a different plane, the Polar to Cartesian conversion needs to be adapted from a 2D environment to a 3D one (total chaos)

To solve these challenges, let’s put our math hats on and dive in:

Differing Radii:

  • An easy fix, these can be stored in an object for each sphere which we can access later in our function

Converting Polar to Cartesian:

  • In 2D Cartesian Coordinates we measure points by their distance away from the x axis (y = 0) and the y axis (x = 0)
The relationship between Polar and Cartesian Coordinates, Source: MattLoftus
  • In 2D Polar Coordinates we calculate the distance away from the origin (0, 0) by measuring the length of the radius (r) and the angle Ø (theta), which extends from the x axis to the radius vector (connecting our point to the origin)
  • To draw an orbital path for our spheres we need to increment the angle (our theta value) in every page refresh, and keep our radius constant
  • For each of these calculations we can run some trig functions to get the values we are after, in our case our x coordinate is r multiplied by cos(Ø) and our our y coordinate is r multiplied by sin(Ø)

The code we need to run these calculations includes a few different additions:

// Trigonometry Constants for Orbital Paths
let theta = 0;
const dTheta = 2 * Math.PI / 100;

These two lines define our:

  1. theta: The variable we will use to store the theta angle for the current page refresh (we use let here as it will be changing each time). It starts at 0
  2. dTheta: The angle constant we will be incrementing theta by is set to 2π / the number of increments we want per revolution. The lower this number (in our case 100) the faster the sphere will orbit the box

The last piece of the puzzle for this section is to increment our theta angle in our rendering function:

theta += dTheta; // <- THIS MUST BE INSIDE OUR RENDERING FUNCTION

Adapting From a 2D to a 3D Environment:

This part was a bit tricky, but after a lot of trial and error, I was able to find a range of values to use in our trig functions that would produce four unique orbital paths, each in a different plane. We can store these in an array of objects:

// Store trig functions for sphere orbits // MUST BE INSIDE RENDERING FUNCTION OR 
// THETA VALUES ONLY GET SET ONCE
const trigs = [
{x: Math.cos(theta*1.05),
y: Math.sin(theta*1.05),
z: Math.cos(theta*1.05),
r: 2},
{x: Math.cos(theta*0.8),
y: Math.sin(theta*0.8),
z: Math.sin(theta*0.8),
r: 2.25},
{x: Math.cos(theta*1.25),
y: Math.cos(theta*1.25),
z: Math.sin(theta*1.25),
r: 2.5},
{x: Math.sin(theta*0.6),
y: Math.cos(theta*0.6),
z: Math.sin(theta*0),
r: 2.75}
];

In our trigs array, we store an object for each sphere’s orbital path, defining a value for the x, y and z coordinates as well as the radius (r) that will be used in our trig functions below:

// Loop 4 times (for each sphere), updating the position 
// MUST BE INSIDE RENDERING FUNCTION
for (let i=0; i<4; i++) {
sphereMeshes[i].position.x = trigs[i]['r'] * trigs[i]['x'];
sphereMeshes[i].position.y = trigs[i]['r'] * trigs[i]['y'];
sphereMeshes[i].position.z = trigs[i]['r'] * trigs[i]['z'];
};

Another for loop? most definitely!

In this loop we run one iteration for each sphere, using our equation from above. We can access the unique radius value and multiply it by our cos(Ø) and sin(Ø) values (and the chosen multiplier constants, e.g. 1.05, 0.8, 1.25, 0.6 and 0) in each iteration. The key to converting our 2D path to a 3D one is to add cos(Ø) or sin(Ø) to the z coordinate (at least one of x, y or z needs to be cos(Ø) if the other two values are sin(Ø) and vice versa to ensure they orbit around the origin).

I would highly encourage you to experiment by changing the placement of the cos and sin ratios in the trigs array and the multiplier constants to see what effect each component has on the overall animation.

And We’re Done!

If you have made it this far, congratulations are in order! You should now have your very own beautiful, colourful, spinning cube, complete with orbiting spheres!

Below is a sample of the full code solution (if you want to view this in an IDE-style environment, you can click the ‘JS’ button in the pen at the top of the page):

import * as THREE from '../node_modules/three/build/three.module.js';
import { TrackballControls } from '../node_modules/three/examples/jsm/controls/TrackballControls.js';
// Scene
const scene = new THREE.Scene();
// Camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.6, 1200);
camera.position.z = 5; // Set camera position
// Renderer
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setClearColor("#233143"); // Set background colour
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement); // Add renderer to HTML as a canvas element
// Make Canvas Responsive
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight); // Update size
camera.aspect = window.innerWidth / window.innerHeight; // Update aspect ratio
camera.updateProjectionMatrix(); // Apply changes
})
// Create box:
const boxGeometry = new THREE.BoxGeometry(2, 2, 2); // Define geometry
const boxMaterial = new THREE.MeshLambertMaterial({color: 0xFFFFFF}); // Define material
const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial); // Build box
boxMesh.rotation.set(40, 0, 40); // Set box initial rotation
scene.add(boxMesh); // Add box to canvas
// Create spheres:
const sphereMeshes = [];
const sphereGeometry = new THREE.SphereGeometry(0.1, 32, 32); // Define geometry
const sphereMaterial = new THREE.MeshLambertMaterial({color: 0xC56CEF}); // Define material
for (let i=0; i<4; i++) {
sphereMeshes[i] = new THREE.Mesh(sphereGeometry, sphereMaterial); // Build sphere
sphereMeshes[i].position.set(0, 0, 0);
scene.add(sphereMeshes[i]); // Add sphere to canvas
}
// Lights
const lights = []; // Storage for lights
// const lightHelpers = []; // Storage for light helpers
// Properties for each light
const lightValues = [
{colour: 0x14D14A, intensity: 8, dist: 12, x: 1, y: 0, z: 8},
{colour: 0xBE61CF, intensity: 6, dist: 12, x: -2, y: 1, z: -10},
{colour: 0x00FFFF, intensity: 3, dist: 10, x: 0, y: 10, z: 1},
{colour: 0x00FF00, intensity: 6, dist: 12, x: 0, y: -10, z: -1},
{colour: 0x16A7F5, intensity: 6, dist: 12, x: 10, y: 3, z: 0},
{colour: 0x90F615, intensity: 6, dist: 12, x: -10, y: -1, z: 0}
];
for (let i=0; i<6; i++) {
// Loop 6 times to add each light to lights array
// using the lightValues array to input properties
lights[i] = new THREE.PointLight(
lightValues[i]['colour'],
lightValues[i]['intensity'],
lightValues[i]['dist']
);

lights[i].position.set(
lightValues[i]['x'],
lightValues[i]['y'],
lightValues[i]['z']
);

scene.add(lights[i]);
// Add light helpers for each light
// lightHelpers[i] = new THREE.PointLightHelper(lights[i]);
// scene.add(lightHelpers[i]);
};
//Trackball Controls for Camera
const controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 4;
controls.dynamicDampingFactor = 0.15;
// Axes Helper
// const axesHelper = new THREE.AxesHelper(5);
// scene.add( axesHelper ); // X axis = red, Y axis = green, Z axis = blue
// Trigonometry Constants for Orbital Paths
let theta = 0; // Current angle
// Angle increment on each render
const dTheta = 2 * Math.PI / 100;
// Rendering Function
const rendering = function() {
// Rerender every time the page refreshes (pause when on another tab)
requestAnimationFrame(rendering);
// Update trackball controls
controls.update();
// Constantly rotate box
scene.rotation.z -= 0.005;
scene.rotation.x -= 0.01;
//Increment theta, and update sphere coords based off new value
theta += dTheta;
// Store trig functions for sphere orbits
// MUST BE INSIDE RENDERING FUNCTION OR THETA VALUES ONLY GET SET ONCE
const trigs = [
{x: Math.cos(theta*1.05), y: Math.sin(theta*1.05), z: Math.cos(theta*1.05), r: 2},
{x: Math.cos(theta*0.8), y: Math.sin(theta*0.8), z: Math.sin(theta*0.8), r: 2.25},
{x: Math.cos(theta*1.25), y: Math.cos(theta*1.25), z: Math.sin(theta*1.25), r: 2.5},
{x: Math.sin(theta*0.6), y: Math.cos(theta*0.6), z: Math.sin(theta*0), r: 2.75}
];
// Loop 4 times (for each sphere), updating the position
for (let i=0; i<4; i++) {
sphereMeshes[i].position.x = trigs[i]['r'] * trigs[i]['x'];
sphereMeshes[i].position.y = trigs[i]['r'] * trigs[i]['y'];
sphereMeshes[i].position.z = trigs[i]['r'] * trigs[i]['z'];
};
renderer.render(scene, camera);
}
rendering();

Thanks for checking out this tutorial, and catch you next time!

— Waldow.the.Dev

--

--