Photo by Diego PH on Unsplash

Adding a Custom Star-Field Background with three.js

Kuntal Das
Nerd For Tech
Published in
7 min readMay 5, 2021

--

Today I’m going to add a Star Field like background which I teased in my previous post Taking a simple Contact Form to the next level with css animation and three.js which will respond to mouse movements.

I will be starting where I left of in the previous post and continue adding on to it.You can also start with a basic blank HTML page if you just want to create the background with three.js.

What is three.js ?

Three.js is a lightweight cross-browser JavaScript library/API used to create and display animated 3D computer graphics on a Web browser. Three.js scripts may be used in conjunction with the HTML5 canvas element, SVG or WebGL.

Add canvas tag : As I decided to use HTML5 canvas element, We need to add a <canvas> tag in the HTML right after the <body> tag [<canvas id=”c”></canvas>] I also added a id with it to reference it in my JavaScript.

A bit CSS : this bit of css will make it span across the whole screen by applying position: absolute; and width:100%;height:100%;. Here it is relative to the body of the HTML so width:100%; will signify 100% width of the body element and same for the height. a z-index:-1; will place it behind all the HTML elements.
Also if noticed the transition property it is there to make the background appear smoothly as it will require some amount of computation after the page and JavaScript is loaded, it will also give the low-end computers sometime to do the computations before rendering the first image.

#c {
display: block;
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 1500ms ease-out;
}

Wire-up the JS : We will make the opacity:1; with the help of js

const canvas = document.getElementById("c");document.addEventListener("DOMContentLoaded", () => {
canvas.style.opacity = 1;
});

Now lets stated with threejs first we need to import it in our project by
import THREE form “https://cdnjs.cloudflare.com/ajax/libs/three.js/r127/three.min.js
or we can simply link it with a script tag in our HTML
<script src=“https://cdnjs.cloudflare.com/ajax/libs/three.js/r127/three.min.js”>

To threejs work on the canvas we defined in the HTML we need to pass it when creating the renderer:

const starFieldBG = () =>{
const renderer = new THREE.WebGLRenderer({canvas});
renderer.setClearColor(new THREE.Color("#1c1624"));
...
}

setClearColor() function set the color of canvas on which we will be drawing. renderer takes a scene object and a camera object as parameters to render it properly.

const scene = new THREE.Scene();const fov = 75, aspect = 2, near = 1.5, far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
renderer.render(scene, camera);

Here I have used PrespectiveCamera which gives a 3d view where things in the distance appear smaller than things up close. fov is the Field Of View in degrees, aspect is the width/height ratio and near far will determine how much space will be rendered.

To better understand about camera in three.js look at this live example at https://threejsfundamentals.org

At this point we wont be able to see anything as there is no light and no objects in the scene. There are lots kinds of lights in the Three.js library like AmbientLight, AmbientLightProbe, DirectionalLight, PointLight etc. You can learn more about lights here. I’ve chosen to work with directional light as it creates light effects like the light is coming from sun.

// light source
const color = 0xffffff, intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);

color & intensity refers to the color of the light and how much it will enlighten the surface respectively. position.set(x, y, z) sets the position of the light-source in the 3d space. Though the light source will not be rendered but its interaction with the objects will be rendered.

Now lets add a object so we can see at least something in the canvas. For that we need Mesh, it is combination of :

  • A Geometry — the shape of the object
  • A Material — how to draw the object, shiny or flat, what color, what texture to apply. Etc.
  • The position, orientation, and scale of that object in the scene relative to its parent. In the code below that parent is the scene.

Lets first draw a cube : this will draw a cube with BoxGeometry in the point(0, 0, 0) to see it we need to camera behind a bit with camera.position.z = 2; Here I’m only changing the z axis value, you can try x or y with some different unit too.

const boxWidth = 1, boxHeight = 1, boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const material = new THREE.MeshBasicMaterial({color: 0x44aa88});const cube = new THREE.Mesh(geometry, material);scene.add(cube);

Make it responsive: Probably by now you might have problem visualizing the cube in your monitor or may be it is problematic when you resize your browser. To fix that we need to know when the window width and window height is changing and make adjustments according to it.

const resizeRendererToDisplaySize = (renderer) => {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
// resize only when necessary
if (needResize) {
//3rd parameter `false` to change the internal canvas size
renderer.setSize(width, height, false);
}
return needResize;
};

This function will remove the scaling problem by resizing our canvas’ drawingbuffer size(canvas’s internal size, its resolution) with renderer.setSize(). “drawingbuffer” of can be compared with viewbox of a <svg> element or image inside an <img> tag (image dimension can differ from the dimension of the <img> tag).

While calling this function inside our main() we will utilizing its return statement to change the aspect ratio of the camera.

const render = (time) => {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
// changing the camera aspect to remove the strechy problem
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
// Re-render the scene
renderer.render(scene, camera);
// loop
requestAnimationFrame(render);
};
requestAnimationFrame(render);

As we need keep tracking out window dimension and call this code block to make the canvas responsive across all screens, I enclosed this code block inside a function and made a recursive loop with requestAnimationFrame() which is provided by our browser.

Add some interactivity: with mouse pointer position. To do that I’ve attached a mousemove EventListener to the document and update two global variable mouseX & mouseY.

// mouse
let mouseX = 0;
let mouseY = 0;
document.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});

And now add two lines of code inside the render() to change it rotation based on mouse movement.

const render = (time) => {
...
...

cube.rotation.x = mouseY * 0.005;
cube.rotation.y = mouseX * 0.005;

renderer.render(scene, camera);
// loop
requestAnimationFrame(render);
};

You will end up something like this.

Up till now I used a cube to make the responsive part easy to understand. But we don’t need a cube we need points so we’ll use THREE.Points in the place of THREE.Mesh and to make it work we need to use a different material too, PointsMaterial now you’ll see 8 points in the 8 corners of the cube. Well need lot more points than that. Just like this:

Now use size:0.05 to make it obvious

const material = new THREE.PointsMaterial({
size: 0.05,
color: 0x44aa88 // remove it if you want white points.
});

We also need lots of points box-geometry will not be much helpful in this case so we need use BufferGeometry()

const geometry = new THREE.BufferGeometry();

BufferGeometry() doesn’t take parameters while creating the geometry but we need change some attributes to define our points location.

geometry.setAttribute(
"position",
new THREE.BufferAttribute(getRandomParticelPos(350), 3)
);

this part sets position by using a array of random float values which is returned by getRandomParticelPos()which takes number of points as parameter. BufferAttribute uses that array to set the position attribute by taking 3 values per point from the array.

const getRandomParticelPos = (particleCount) => {
const arr = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
arr[i] = (Math.random() - 0.5) * 10;
}
return arr;
};

this will look something like this:

To change the hover effect a bit we need to change this two lines in the render() and remove the color property from the material to make it white.

...
const render = (time) => {
...
...

//cube.rotation.x = mouseY * 0.005;
//cube.rotation.y = mouseX * 0.005;
cube.position.x = mouseY * 0.0001;
cube.position.y = mouseX * -0.0001;

renderer.render(scene, camera);
// loop
requestAnimationFrame(render);
};
...

Unfortunately we can’t make the points round or any shape like that. But we can use texture to make them look like whatever we want. I made two PNG images in Figma to use as texture and uploaded them in a GitHub repo. To use textures we need TextureLoader() , we also need to change the object we passed in to the PointsMaterial() while creating the material.

const loader = new THREE.TextureLoader();
...
const material = new THREE.PointsMaterial({
size: 0.05,
map: loader.load(
"https://raw.githubusercontent.com/Kuntal-Das/textures/main/sp2.png"
),
transparent: true
// color: 0x44aa88
});

loader.load() creates texture from the given PNG file and passes it as map to make the points look like it but we also need transparent:true to make the previously rendered white square disappear.

Phew We made it finally. This background only.

In the this final version I added two different texture with different no of points. This post is has become already very long I don’t want to make it more longer, So I’ll leave it to yourself to implement and get your hands dirty.(or just have a peak at my code 🤭)

Now who were following along from the previous post they should have this:

So we today learned :

  • How to use threejs with canvas.
  • We learned we need scene and camera for the renderer render our scene.
  • Also we need to add light to the scene to see the objects we add to the scene.
  • We learned about creating Mesh with BoxGeometry& MeshBasicMaterial.
  • We made the image inside canvas Responsive by listening to window width and height.
  • How lots of points can be rendered BufferGeometry & PointsMaterial.
  • We learned a little bit about textures in threejs.
  • And we also learned about two ways to make canvas element interactive.

--

--