3D Starry Night with Three.js

Image for post
Image for post
Photo by Jeremy Thomas on Unsplash

This tutorial introduces you to making 3D graphics and using textures with the JavaScript library Three.js. Although knowledge of basic JavaScript and 3D helps, it is not strictly necessary to follow along.

**If you get stuck at any point along the way, compare your code to the solution code!

The amazing thing about being a web developer (or coding hobbyist) in 2018 is everything you can do simply in the web browser. No annoying software downloads here — once you paste the Three.js library into a local JavaScript file, you can start making 3D graphics almost immediately!

Setting up our environment

First, we need to set up a basic environment where we can start developing in HTML/CSS/JavaScript and load in some assets like images. If you have a preferred environment you want to use, skip this section. For everyone else, make an account on Cloud9 if you don’t already have one. Log in with your GitHub if you can.

Image for post
Image for post

Create a new workspace with an HTML5 template. Call it whatever you want.

Image for post
Image for post
Image for post
Image for post

Inside the workspace, find the bash area below the README:

Image for post
Image for post
Look for the dollar sign! $

With a few bash commands, we’ll create a new directory for our project and add some files. I’m going to call my directory “starry-night” and create files for the HTML, JavaScript, and Three.js library.

To create the folder, type:

mkdir starry-night

Now navigate into that folder (“cd” stands for “change directory”):

cd starry-night

Now let’s create our files inside that folder:

touch index.html script.js three.js

We’ve created all our files! Open the starry-night folder from the lefthand file menu and open up the three.js file.

Image for post
Image for post
The three.js file in my workspace, populated by the scary library code.

Paste the contents of the Three.js library into the three.js file. Save it and close it. You’re done with three.js for now.

Now open the index.html file. Paste in this HTML skeleton:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Starry Night</title>
<style>
body { margin: 0; }
canvas { width: 100%; height: 100% }
</style>
</head>
<body>
<script src="three.js"></script>
<script src="script.js"></script>
</body>
</html>

We won’t be doing anything more with HTML in this tutorial, but you can see how it creates a canvas the size of the browser window. Then it hooks up the JavaScript files to the HTML page so they can load 3D graphics onto the canvas.

Don’t close index.html yet, but let’s open script.js and get started for real.

Making a scene

Before we can render a sphere onto our screen, we need to set up the scene, camera, and renderer. These items will render our JavaScript graphics on the HTML page.

Three.js has excellent documentation and provides this starter code for setting up our scene:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement );

Although the documentation explains this code thoroughly, I will just tell you that we have created a scene and given our camera a 75-degree field of view. Our camera also looks from 0.1 to 1000 units away (pretty far!). Finally, our renderer spans the entire width and height of the browser window and has been appended to the DOM, meaning that our graphics will appear on the HTML page.

To preview your scene so far in Cloud9, you’ll need to navigate back to index.html and press the “Preview” button up top.

Image for post
Image for post

You can also pop out the scene into a new window by pressing the square button.

Image for post
Image for post

Although we have done important work here, our full-screen scene is not rendering:

Image for post
Image for post
What is this?

We need to write a function that renders the entire scene. Add this animate function to the end of your code:

function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();

In preparation for moving graphics, the animate function repeatedly asks for new animation frames and renders the scene. The function call at the end, denoted by the parentheses (), sets the animation into motion.

Because we haven’t added any graphics yet, you should now have a black void. Yay!

Image for post
Image for post
A black void

Adding a sphere

3D shapes like spheres are made out from meshes of little triangles.

Image for post
Image for post
Even this dolphin mesh is made out of triangles! (source)

When we define a mesh, we have say what kind of geometry we want (sphere? cube? etc) and what kind of material (color? texture?).

I’m going to say I want a sphere with a radius of 0.5 and a white material. Here are the variables to store the sphere’s geometry and material:

var geometry = new THREE.SphereGeometry( 0.5, 32, 32 );
var material = new THREE.MeshBasicMaterial( { color: 0xffffff } );

The number 32 that appears twice after the radius tells the computer how many width and height segments to use to comprise the sphere. The higher the number, the more rounded the sphere appears — because, after all, it’s a mesh comprised of pointy triangles.

For the colored material, you can see by the 0x that comes before the next six digits that we’re dealing with hex color values. In this case, ffffff is white.

Now I can use the geometry and material to make a sphere, which I think will be a planet in this starry night scene.

var planet = new THREE.Mesh( geometry, material );
scene.add( planet );

Now I’ve created a planet out of the geometry and material I defined before and added it to the scene!

However, my default location is (0, 0, 0), so if I view the scene now, I’m inside the sphere and don’t see it.

Instead I can zoom out by changing my z-position so that I’m 10 units away from the origin, where our sphere appears by default:

camera.position.z = 10;

Like in high school geometry, we have the x-axis going side-to-side and y-axis going up and down, but in 3D and VR, we also get the z-axis which allows the user to move back and forth throughout the scene. When we change our z-position to 10, we’re backing up.

Now you should see this sphere in the middle of your scene:

Image for post
Image for post

Congratulations on making your first 3D object!

Using textures

OK, so actually our white sphere is kind of boring. Let’s spruce it up a little with a texture.

A texture is when we wrap a 2D image around a 3D object to make it more interesting. Because I want my sphere to be a planet in the starry night, I took a look at this awesome planet stock art and cropped myself a square of material to tile on my sphere.

Crop your own square of cosmic art and save it as something sensible, like “planet.png” (or whatever your file format is). I called mine “neptune.png” because it was a picture of Neptune. Now drag that file right into your starry-night directory on Cloud9.

Image for post
Image for post

Now we can use that file to make a texture!

Delete the line from your code that starts with var material. Instead of making our sphere white, we’re going to use our texture.

Now, where the var material line was, write instead:

var planetTexture = new THREE.TextureLoader().load( "neptune.png" );
planetTexture.wrapS = planetTexture.wrapT = THREE.MirroredRepeatWrapping;
planetTexture.repeat.set( 2, 2 );
var material = new THREE.MeshBasicMaterial( { map: planetTexture } );

If your image was saved as something other than “neptune.png,” change that part of the code to reflect your image name.

Basically, this code loads in a texture and saves it in the variable planetTexture. Then we edit the texture to enable mirrored repeat wrapping (which takes away that awful tiled look) and specify how many times to repeat the texture. (Read more in the docs.) Finally, instead of assigning a plain old color to the material, we assign our cool new texture.

Make sure that the line of code that says var planet = new THREE.Mesh( geometry, material); appears AFTER we’ve defined this new texture and material!

Now you should have a shiny cosmic planet:

Image for post
Image for post

Adding in a thousand stars

Okay, not a thousand stars, but maybe 200 or so.

First, we need to find a texture for our stars. I did a Google Image search for “star” filtering for transparent images, and I found this image. Whatever image you decide on, drag it to your desktop and then upload it into your starry-night directory.

Now we can load that texture into our code! Paste this code in before the animate function:

var starTexture = new THREE.TextureLoader().load( "star.png" );

If your star image file is called something other than “star.png,” change that part of the code.

I’m going to be creating about two hundred stars, so I don’t want to define each star variable individually. Instead I will store them all in a data structure called an array, which is basically like a big bucket you can put lots of other variables in (traditionally variables of the same type, but JavaScript is more flexible here than other languages).

Before I fill my array with stars, let’s define an empty array. Continue to code outside the animate function:

var stars = [];

All right, so now, the same way we created our single sphere, we are going to create TWO HUNDRED STARS. To accomplish this, I will use a for loop that repeats 200 times, putting each star at a random location using (x, y, z) coordinates between -10 and 10.

But first, I need to fashion myself a function that generates a random number between -10 and 10. After reading this Stack Overflow post, I adapted the code into the function below, which does exactly that. Paste this function definition at the bottom of your code:

function getRandom() {
var num = Math.floor(Math.random()*10) + 1;
num *= Math.floor(Math.random()*2) == 1 ? 1 : -1;
return num;
}

Okay, now we can start looping to generate stars at random locations. This for loop runs 200 times, and each time, it creates a star at a random point and adds it to our stars array. Add it right after the declaration of the empty stars array:

for (let i = 0; i < 200; i++) {
let geometry = new THREE.PlaneGeometry( 0.5, 0.5 );
let material = new THREE.MeshBasicMaterial( { map: starTexture } );
let star = new THREE.Mesh( geometry, material );
star.position.set( getRandom(), getRandom(), getRandom() );
star.material.side = THREE.DoubleSide;
stars.push( star );
}

To delve in a little deeper, you can see that each star is actually a 0.5 x 0.5 plane with a double-sided star texture on top of it. Its position is three random coordinates between -10 and 10.

However, none of these stars appear yet because we haven’t used the scene.add() function like we did with the sphere. But how do we add 200 stars to the screen separately? Another for loop, of course, following the previous for loop:

for (let j = 0; j < stars.length; j++) {
scene.add( stars[j] );
}

Now our scene is full of beautiful stars!

Image for post
Image for post

Adding motion

But our scene is still a little…static. We are using this advanced JavaScript 3D graphics library — shouldn’t there be some animation?

Absolutely! The first thing we can do is make our stars rotate. To do that, we can add a for loop inside our animate function that loops through all the stars and changes their rotation incrementally in every new frame.

Inside the animate function, before the requestAnimationFrame call, add in this loop:

for (let k = 0; k < stars.length; k++) {
let star = stars[k];
star.rotation.x += 0.01;
star.rotation.y += 0.01;
}

To simulate the stars twinkling, we can also continually vary their lightness. Using HSL colors (HSL stands for hue-saturation-lightness), we can cycle from 0% to 100% lightness.

First, outside the animate function, we want to define our lightness variable. Before the animate function definition begins, write:

var lightness = 0;

Now, inside the for loop in the animate function, we’re going to adjust the lightness. After the adjustments to the star’s rotation, add in this code:

lightness > 100 ? lightness = 0 : lightness++;
star.material.color = new THREE.Color("hsl(255, 100%, " + lightness + "%)");

Using the ternary operator, we continually increase the lightness until it hits 100, and then start it over at 0 again. Then we adjust the star’s lightness inside its color attribute accordingly.

Now your stars should be twinkling!

Image for post
Image for post
You can make the stars twinkle less aggressively if you increment the lightness by a decimal, like 0.05. In contrast, the “++” operator in the code means we’re incrementing by 1.

Adding camera motion

I don’t know about you, but I’m curious to see what the scene looks like from more angles other than head-on. I’d like to move the camera and circle around the planet, enjoying all the twinkling stars that go by.

Turns out you need some MATH to figure out how to move in a circle! Luckily, through Googling, I arrived at this quick tutorial about how to move the camera in a circle around a single axis (here, the y-axis).

First, we want to define the rotation speed. Outside the animate function, right after where you defined var lightness, define another variable:

var rotSpeed = 0.01;

Now, to rotate around the planet and move through the stars, add this code into your animate function. Put it after the for loop and before the requestAnimationFrame call:

let x = camera.position.x;
let z = camera.position.z;
camera.position.x = x * Math.cos(rotSpeed) + z * Math.sin(rotSpeed);
camera.position.z = z * Math.cos(rotSpeed) - x * Math.sin(rotSpeed);
camera.lookAt(scene.position);

As you can see, we are adjusting the camera’s current x-position and z-position to move it around in a circle around the y-axis. Then we focus the camera on the scene at (0, 0, 0).

Are you circling your planet in outer space yet?

Image for post
Image for post

If so, nice work! If not, take a look at the solution code and check yours for errors.

Congratulations on creating a beautiful starry night scene in Three.js!

View the solution code and the final starry night scene.

Software developer & educator

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store