An intro to three.js!
By Angel Dollface at 2023/01/27
Introduction
When browsing the web I'm sure you've seen some fancy websites that have some 3D graphics in them. Have you ever stopped and asked yourself how these graphics are made? Well, after you've completed this tutorial, you will know some coding in Javascript, be very much wiser, and hopefully have an idea on how to make fancy 3D animations of your own!
Requirements!
For this post I'm assuming you have no experience in coding at all. You do, however, need to install Node.js! and VS Code. We will be using Javascript for this tutorial so you will need Node.js. Because Javascript is basically just code written in text format, we will also need a text editor. This is why we're using VS Code. Finally: Before we dive in, allow me to explain some key concepts that we will be using in this tutorial!
Concepts
Before we start I need to explain the following concepts to you: browser, runtime, HTML, CSS, Javascript, classes, variables, components, and functions! So buckle up! This section is important!
A browser is an application used to display web pages and browse the internet. It can interpret and display three things: HTML, CSS, and Javascript. HTML defines the structure of a web page: which text goes where, which heading under what section, etc. You could make a website without CSS and Javascript but that website would look a bit boring. To make the website a bit more stylish we use CSS. CSS gives the parts of the structure (HTML) some style. You can set where a part of this structure is supposed to go on the page, change the size, font family, font size, and a whole lot of other things. One part of this structure is called an element. Finally, we come to Javascript! Javascript is a programming language developed in the 1990s to make websites in the browser interactive. It has come a long way since then. These days it can also be used to do other things but to do those things, we need a runtime! This runtime is Node.js it can run code written in Javascript and execute whatever the code tells it to wherver, not neccessarily in a browser. All clear so far?
Since Javascript is a programming language we need to understand some key programming concepts! The first concept is that of a variable! Variables are like a box. You can store anything in this box. And because we like to be organized, we have to give this box a name. So if we had a cat named "John", a variable pertaining to the cat would look something like this:
let catName: string = 'John';
But what is this string
nonsense I hear you asking? Well, remember, programming languages are languages we use to talk to computers. And computers are pretty dumb. Because our code is data, we need to tell the computer what sort of data it is dealing with. In Javascript there are several data types, the most important of which are: strings, booleans, and numbers. Strings are words or phrases surrounded by single quotes or double quotes. (Single quotes are a bit more stylish and I'm a stylish dolly.)Booleans indicate whether something is true
or false
. Numbers? Well, those speak for themselves.
Next concept! Functions! Functions are re-usable blocks of code. So basically like a box that does something. A function to greet the world would look like this:
function hello(name: string): void {
let msg: string = 'Hello, ' + name + '!';
console.log(msg);
}
We declare a new function with the function
keyword, give it a name, hello
, tell Javascript that it accepts one argument of type string (name: string
) and doesn't return any data (void
). In the function body, the bit between the curly braces, we first define a variable msg
of type string which holds the string "Hello" and whatever string is passed in as name
. If I wanted to greet myself, I would call hello
like this: hello('Angel');
. If I were to run hello('Angel');
, "Hello, Angel!" would be printed to the console. All clear so far? Good!
Pen-ultimate concept! Classes! A class is also just a box that does something. It is a fusion of the variable box and the function box! How does this work? Allow me to illustrate!
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
printName(): void {
console.log(this.name);
}
}
Classes are a fusion of the variable box and the function box, in that, you can save data in a class and do something with that data inside functions declared inside that class. In the snippet above, we declare a class with the class
keyword, name it with Person
and define everything inside it inside an opening curly brace and a closing curly brace. This is called the class body. Inside this body we have three parts: The field section, the initializer, and the class methods (functions). The field section holds the fields of the class. Think of the class like a table where we say "Make a column that holds names.". In the example above, the class has one field: name
of type string. The initializer is the function that is present in every class and runs when the class is used. We can supply arguments in this function and set class fields to be some value from these arguments. In this example, we accept one argument, name
. We set this to be the class's name
field. The function printName
is called an associated function. We do not have to use the function
keyword to declare this function. Our printName
function does one thing: It prints out the class's name field. So, how would we actually use this class?
let dollPerson: Person = new Person('Angel');
dollPerson.printName();
We fill our class "table" with some data: 'Angel'
. This is called "instantiating" of a class. Then we call the printName
on our instance. This should print out "Angel" to the console.
Final concept: Components! Some years ago, when Node.js was invented, someone thought: "What if we made something that would allow web developers to segment different parts of a web page into different bits of code?" Thus, React.js was born. React.js is what's called a library. Libraries are a bunch of functions, classes, and variables that someone else has written and made available for re-use by others. React.js makes it possible to develop a website using components. These components are classes that hold some HTML elements and are called when you "render" a React.js app. In this tutorial we will be using React.js to build our glowy cube.
Note: When I say "print" or "console" I do not mean actually printing out things on a piece of paper on your printer. I mean that text is output to what is called a "terminal". This terminal is a text representation of the applications, files, and folders that are on your computer. Terminals give developers a fast and easy way to analyze the output of different applications and write new ones.
Brilliant! You just had a crash course in Javascript development! Let's get to making our cube in React.js!
Creating a React.js app!
The first thing we will need to do to render our fancy cube in a browser is make a small web application. We will make this using a library called React.js. For this tutorial, we will be writing our own component. But to do this we will first need to install a few things. Since I'm assuming that you've successfully installed Node.js you should also have access to one of Node's sub-commands, NPM. NPM is Node's package manager. It helps us to install libraries others have written in Javascript and made available for re-use. We will be using a dialect of Javascript, called Typescript. Since normal Javascript has no strict data types and this can make writing code in it very hard and messy, some people at Microsoft sought to solve this problem by writing an application that compiles a dialect of Javascript with strict data types into normal Javascript. (The browser only understands vanilla Javascript.) Finally, you will also have to know how to open a command prompt on your system. If you don't, google the following phrase: "How do I open a command prompt on 'your operating system'?", where 'your operating system' represents your operating system. Open a command prompt and type the following to install Typescript's compiler with this command:
npm install -g typescript
Next, we will be creating a basic React.js web app that uses Typescript with this command:
npx create-react-app glow-cube-tutorial --template typescript
Next change directory into the glow-cube-tutorial
directory with this command:
cd glow-cube-tutorial
Open the project in VS Code with this command:
code .
House Keeping!
After you've done that, you should see these files and directories on the left of the window that just opened up:
├── README.md
├── package-lock.json
├── package.json
├── node_modules
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ └── setupTests.ts
└── tsconfig.json
Go ahead and delete the following files in the src
directory:
App.css
, App.test.tsx
, index.css
, App.tsx
, logo.svg
, reportWebVitals.ts
, and setupTests.ts
.
Next, delete favicon.ico
, logo192.png
, and logo512.png
in the public
directory. We won't be needing these files.
We do need to customize index.html
in the public
directory, so select all the text in index.html
and delete it. Fill it with this code instead:
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="My first three.js project!"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Glow Cube Tutorial</title>
</head>
<body>
</body>
</html>
We will also need to customize manifest.json
a bit, delete everything in manifest.json
and fill it with this code instead:
{
"short_name": "Glow Cube Tutorial",
"name": "Glow Cube Tutorial",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
Deleting files is done by clicking the directory, then right-clicking the file, and then clicking Delete
.
Next, we need to delete the node_modules
folder. After this open package.json
. Select everything and delete that. Replace it with this code:
{
"name": "glow-cube-tutorial",
"description": "My first three.js project!",
"version": "1.0.0",
"private": true,
"dependencies": {
"@types/node": "^16.18.2",
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.8.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/three": "^0.144.0",
"postprocessing": "^6.29.1",
"sass": "^1.55.0",
"three": "^0.146.0"
},
"homepage": "/glow-cube-tutorial/"
}
Create a file called ModelCog.tsx
in the src
directory. You can do this by right-clicking under src
and clicking New File
. Next, create a file called global.scss
also in the src
directory. Finally, create a file called render.ts
in the same driectory. This file hold all our three.js magic.
Put this in index.tsx
:
import './global.scss';
import React from 'react';
import ModelCog from './ModelCog';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(
document.body as HTMLElement
);
root.render(
<React.StrictMode>
<ModelCog/>
</React.StrictMode>
);
This file is the main entry point for React.js. It will attempt to render all components specified in the <React.StrictMode>
tags to our web page. The first line imports a stylesheet, a dialect of CSS which is like CSS but with some other useful functionality. The next import is us importing React
to manage our web application "under the hood". After this we import our 3D component so that we can use it in the <React.StrictMode>
tags. Finally, we import something called React's virtual DOM but we don't need to worry about this too much. It won't be too relevant. In the line starting with const
we make a virutal DOM that we can use and tell it to render our component with our model in root.render
.
Let's move on to our 3D model component!
Put this in ModelCog.tsx
:
import React from 'react';
import { ReactElement } from 'react'
import { renderModel } from './render';
export class ModelCog extends React.Component {
render(): ReactElement {
return (
<>
</>
);
}
componentDidMount(): void {
renderModel();
}
}
export default ModelCog;
In this component we again import React
to do some magic behind the scenes. After this we import what's called a ReactElement
. This is a sort of HTML element but with some extra magic, nothing we need to worry about. The next line is very important. We import a function that will render our model on our web page. After this we finally declare our 3D model component. We do this in a class. Classes can inherit from other classes. (Basically, re-use data and functions from other classes.) This inheritance is signified with the extends
keyword. The React
class gives us a sub-class, Component
, we can use to define our own components. In the next line we declare a sub-function of our ModelCog
component. This function has to be present in every React.js component. It returns our HTML element-cum-React-magic. But since we actually return some data in the renderModel
function (An HTML element.), which we call later, we return an empty element. The second function we declare is the componentDidMount
function. In this function we tell React.js this: Load my model after the component is rendered to the web page. Since 3D models can take some time to load, we need to call renderModel
here. Finally we export our 3D model component to index.tsx
so that we can use it.
Let's move on to some styling!
Put this in global.scss
:
html, body, canvas {
top: 0;
right: 0;
left: 0;
bottom: 0;
width: 100vw;
height: 100vh;
padding: 0px;
margin: 0px;
box-sizing: border-box;
}
index.tsx
will take the styling we define in global.scss
and apply it to our web page. Don't worry about what this does. Just bear with me.
Great! Now we can move on to the juicy bit: our glowing cube! For this we need to edit render.ts
.
Installing and importing three.js!
To use the three.js library we need to re-install our project's dependencies. We can do this by using NPM. To add it, click the "Terminal" button in the task bar at the top of VS Code and click "New Terminal". A smaller sub-window should open in the lower half on the left. Type the following command.
npm install
Good! That has set everything up from Node's and React's side.
"three.js" gives us a whole bunch of classes and functions to make 3D graphics in the browser. Let's import those in render.ts
by adding this line to it:
import * as THREE from 'three';
Next, we need to define our renderModel
function and export it to ModelCog.tsx
.
Add these lines to render.ts
:
export function renderModel(): void {
}
export default renderModel;
Creating a scene!
Great, now here are some three.js basics: Every 3D graphic you see has two essential parts (Not components!): A scene and a camera. A scene is like a blank canvas. It gives us a space in the browser to position things on and do stuff with those things. The camera gives a view of what is happening on our scene. (Kind of like a film camera.) Let's build both of those things! Add these lines to render.ts
inside the renderModel
function.:
let scene: THREE.Scene = new THREE.Scene();
let camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(
75,
window.innerWidth/window.innerHeight,
0.01,
1000
);
camera.position.set(0,0,4);
The first line is a variable we declare to make our scene. We use the class that three.js gives us for this. The next is similar: We re-use the camera class that three.js gives us and give it some data to start with! Congratulations, you just created your first three.js scene and camera! Onwards to the cube!
Adding a "mesh"!
All objects in a scene are made of what's called a mesh material. This is kind of like the plastic of the web. But it is maleable and we can modify it however we want. Because objects in a scene are made of this maleable plastic, these objects are called meshes. Now each mesh has two parts. A geometry and a material. The geomtry decides how the mesh looks and how it is "built" on the screen. The second part is the material. We can have this material be anything we want. So! Let's add our cuboidal mesh! To add our cube, add these lines to render.ts
:
const meshColor: THREE.Color = new THREE.Color(0xFFFFFF)
const geometry = new THREE.BoxGeometry(1,1,1);
const material = new THREE.MeshStandardMaterial({color:meshColor});
const cube = new THREE.Mesh(geometry,material);
cube.material.emissive = new THREE.Color(0xFFFFFF);
cube.material.emissiveIntensity = 10;
cube.position.x = 0;
cube.position.y = 0;
cube.position.z = 1.5;
cube.rotation.y = 1.5;
scene.background = new THREE.Color(0x000000);
scene.add(cube);
In the first line we tell three.js what color our mesh should have. We do this by calling the color class that three.js gives us.
In the second line we use one of three.js's pre-defined geometries, the BoxGeometry
. We tell it that it is supposed to be 1 meter wide, 1 meter tall and 1 meter in depth.
In the third line, we define the cube's material. We use the MeshStandardMaterial
that three.js gives us and supply the color to tell three.js what color we want our cube to be.
In the fourth line we build our cube from the geometry and material that we defined just now by using the Mesh
class from three.js and supplying the geometry and material as arguments.
In the next two lines, we set up the glowing properties of our cube. We tell three.js that we want the cube to glow white and we also tell three.js how strongly our cube should glow.
In lines 7-9 we tell three.js where to place the cube in the scene. We place it roughly in the middle of the scene.
In line 10 we give the scene a background color. The cube is white and the background is black for maximum contrast.
Finally we add the cube to our scene.
Giving the mesh a glow-up!
To make our cube a glow, we need to add a filter to our scene. This filter will make any mesh we specify glow. To do this, add these lines to the top of render.ts
:
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
In the first import we use a special effect handler that three.js has. We need to use this since making meshes glow in a scene isn't really standard fair for most three.js users.
In the second line we import the so-called EffectComposer
, this is basically our filter manager. Think of it as the filter-applying-button in a photo-editing application.
In the final import, we import the glow-up filter for our scene. This filter will make our cube glow!
Making a renderer.
We're almost there! Since 3D graphics are very resource-intensive, we need to use Web GL to render our scene. three.js's renderer is a kind of bridge between Javascript and Web GL. We pass this renderer our scene and camera and it takes care of the rest. Add these lines inside render.ts
under the part where we added the cube.
let renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
In the first line, we make our renderer from three.js so that we can use it to render our scene. We pass it a paramater for better and smoother viewing.
In the second line, we tell the renderer how large the scene should be rendered. Because our web page will span the entire area of the page, we call some in-built variables the browser has, namely window.innerWidth
and window.innerHeight
. Finally, we add the renderer to the list of the web page's HTML elements.
Applying our filters and wiring those up with the renderer.
Good, now we have our scene, camera, mesh, and our renderer! What's missing?
Applying the glowing effect! Because we can't directly apply the glowing effect to our meshes and tell the renderer about the glowing filter, we need a kind of bridge to tie them all together. This bridge is RenderPass
. We tell the effect manager: "Hey use this extra filter!" The effect manager says: "Ok, sure, but how? I'm not powerful enough to do that." We tell the effect manager: "Don't worry, use this tool to do that." "That tool" is RenderPass
. Add these lines below the renderer section in the renderModel
function.
const renderScene = new RenderPass( scene, camera );
const bloomPass = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 1.5, 0.4, 0.85 );
bloomPass.threshold = 0;
bloomPass.strength = 0.8;
bloomPass.radius = 0.001
var composer = new EffectComposer( renderer );
composer.addPass( renderScene );
composer.addPass( bloomPass );
In the first line we create an instance of this special tool: RenderPass
.
In the second line we instantiate our special glowing filter: UnrealBloomPass
.
In lines 3-5 we specify some properties that our glowing meshes should have. These are general properties that are applied to any mesh we specify should glow in our scene.
In line 6 we instantiate the effect manager and tell it to use our renderer from earlier.
Finally we tell the effect manager to use our special tool and then we can add the glowing filter to the effect manager.
Nearly there! Now we need to animate our scene so we can see something!
Animation (Important!)
Because handling 3D models in the browser is a very resource-intensive process, the browser doesn't automatically render our model. To actually see our model we therefore need to redraw our web page continously. This is done by repeatedly calling the browser's in-built requestAnimationFrame
function. We call it continously by supplying the function in which requestAnimationFrame
is called as the argument to requestAnimationFrame
.
Add these lines inside renderModel
:
const animate = () => {
composer.render();
cube.rotation.z -= 0.015;
cube.rotation.y -= 0.015;
requestAnimationFrame(animate);
}
animate();
In the first line, we define a so-called "variable function". These are functions that are declared like normal variables but they follow the same syntax as normal functions. We declare the animate
function in the first line.
In the second line, inside the function's body, we tell the effect manager to re-render our scene.
The next two lines are important! We add a small number to the cube's roation to actually make it move. The unit used here are radians. So once the cube has turned 2* Pi radians, it starts turning again.
In the last line of the function body, we call requestAnimationFrame
.
In the last line of this snippet, we then call the animate function to actually render our scene.
Running our fancy web page!
Good, now we're ready to make some magic happen. Open up the terminal by clicking the "Terminal" button in the task bar of VS Code and type the following command:
npm start
React will build our web page and serve it on your local network. To view your fancy cube, visit this URL: http://localhost:3000/glow-cube-tutorial
Pretty amaying isn't it? And guess, what? You built this!
Showing the world what you've built.
If you'd like to show the world what you've built, create a new directory under glow-cube
and name it as follows: .github
. Inside .github
create another folder, name this folder workflows
. Inside workflows
create a file called react.yml
and put this inside it:
name: React CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [15.x]
steps:
- uses: actions/checkout@v3
- name: Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: "Installing dependencies."
run: npm install
- name: "Running site build."
run: npm run build
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4.2.5
with:
branch: gh-pages
folder: build
Next, head over to GitHub and make an account (If you don't have one already.). Once that is done, click "New Repository". Name the repository this: "glow-cube-tutorial". After this is done go to the "Settings" tab on the repository. Click "Actions" > "General" in the left menu column and find the section called "Workflow permissions". Click the field that says "Read and write permissions". Then check the box above the "Save" button and then click "Save".
Next, open up a terminal in VS Code inside the project we just made and type this command:
rm -rf .git
This will delete any previous Git data inside the porject. Don't worry, we need to do this. If you're on Windows, look up the command to force-delete a directory that isn't empty. I'm doing this on Mac OSX so the command above works for me.
Good, now we can establish a new Git repository. Type the command below:
git init
After this, we need to supply Git with some data for our repository. your-email
represents the email address you used to sign up with GitHub. your-username
represents your GitHub username. Type these commands:
git config --global user.name your-username
git config --global user.email your-email
git branch -M main
git remote add origin https://github.com/your-username/glow-cube-tutorial.git
Finally, to publish our project on GitHub, we need commit and push our changes by executing these commands:
git add -A
git commit -m "My first three.js project!"
git push -u origin main
That's that! Now head back to your reporitory's page on GitHub. Go to the tab that says "Actions". Wait for the "React CI" action to complete. After this, head over to the repository's "Settings" tab. Click on the menu item called "Pages".
Under the heading "Build and deployment" and the sub-heading "Branch" click the little drop-down on the left-most box and select the branch "gh-pages". And then click "Save".
Head back to the "Actions" tab of your repository. Wait for the "pages build and deployment" action to complete.
Congratulations! Your fancy cube web page should now be available to the internet at https://your-username.github.io/glow-cube-tutorial
.
Source and references!
You can find the complete source to this project here. If you have any problems or run into something you don't understand, file an issue here and I'll get back to you as soon as I can! <3
Conclusion.
I hope this tutorial got you interested and excited to build amazing things using three.js. If it did, I'm glad. I would appreciate it if you could give any of my social links a follow and if you really liked this tutorial, I'd appreciate a small donation to my Buy me a coffee! page. The link to that is here. You can find links to my social media accounts on the "Contact" page over the "Menu" button up top.