This repository provides a simple introduction to Three.js, a popular JavaScript library for creating 3D graphics in the browser. Learn the basics of setting up a scene, adding lights, and importing 3D models from Sketchfab.
Before running this project, ensure you have a web server installed. You can choose between using http-server
with npm (Node.js) or the built-in http.server
module with Python.
If you have Node.js installed, you can use http-server
from npm. Install it globally using the following command:
npm install http-server -g
If you have Python installed (which is likely already the case), you can use the built-in http.server
module. No additional installation is required.
git clone https://github.com/math-silva/threejs-introduction.git
cd threejs-introduction
http-server
python -m http.server -b localhost 8080
This will initiate a web server. Access the project by opening your browser and navigating to http://localhost:8080
or another port specified in the terminal.
index.html
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
The importmap
tag is a new feature of the web platform that allows you to import modules from external sources. In this case, we are importing the Three.js library from unpkg.
index.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
three
is the main library, OrbitControls
is a helper for the camera, and GLTFLoader
is tool for loading 3D models.
First, we need to create a scene:
const scene = new THREE.Scene();
Then, create a camera and position it:
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(18, 7, 12);
Now, create a renderer and add it to the DOM:
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
document.body.appendChild( renderer.domElement );
We can also add some helpers to the scene:
const axesHelper = new THREE.AxesHelper( 50 );
scene.add( axesHelper );
const controls = new OrbitControls( camera, renderer.domElement );
The AxesHelper
is just a visual aids to help us position the objects in the scene. The OrbitControls
is a helper for the camera that allows us to move around the scene.
Now, our scene is ready to render:
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
index.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(18, 7, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
document.body.appendChild( renderer.domElement );
const axesHelper = new THREE.AxesHelper( 50 );
scene.add( axesHelper );
const controls = new OrbitControls( camera, renderer.domElement );
controls.update();
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
We will add a ambient light and a spot light to the scene:
const ambientLight = new THREE.AmbientLight( 0xffffff, 0.5 );
scene.add( ambientLight );
const spotLight = new THREE.SpotLight( 0xffffff, 0.7 );
spotLight.position.set(2, 12, 2);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.5;
spotLight.decay = 1;
spotLight.distance = 0;
Attributes can be added to the spot light to enable shadow casting:
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 60;
scene.add( spotLight );
scene.add( spotLight.target ); // add the target to the scene so we can update where the light is pointing
Add a helper to visualize the light:
const spotLightHelper = new THREE.SpotLightHelper( spotLight );
scene.add( spotLightHelper );
Note that we cannot visualize the incidence of light in our scene very well because we do not have any object in the scene to receive the light. Let's create a plane to receive the light:
const planeGeometry = new THREE.PlaneGeometry( 100, 100 );
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xbcbcbc });
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
plane.rotation.x = -Math.PI / 2; // rotate the plane to face up
plane.receiveShadow = true;
scene.add( plane );
Now we can see the incidence of light in our scene.
index.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(18, 7, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
document.body.appendChild( renderer.domElement );
const axesHelper = new THREE.AxesHelper( 50 );
scene.add( axesHelper );
const controls = new OrbitControls( camera, renderer.domElement );
controls.update();
const ambientLight = new THREE.AmbientLight( 0xffffff, 0.5 );
scene.add( ambientLight );
const spotLight = new THREE.SpotLight( 0xffffff, 0.7 );
spotLight.position.set(2, 12, 2);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.5;
spotLight.decay = 1;
spotLight.distance = 0;
spotLight.castShadow = true;
spotLight.shadow.bias = -0.001;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 60;
spotLight.shadow.focus = 1;
scene.add( spotLight );
scene.add( spotLight.target );
const spotLightHelper = new THREE.SpotLightHelper( spotLight );
scene.add( spotLightHelper );
const planeGeometry = new THREE.PlaneGeometry( 100, 100 );
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xbcbcbc });
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true;
scene.add( plane );
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
We will import a 3D model from Sketchfab, you can choose any model you want, but for this tutorial we will use the "Deer walk" model, by LostBoyz2078, because it has a walking animation that we can use.
This model has a .gltf
file, so we will use the GLTFLoader
to import it:
let deer; // this will hold the deer model
let mixer; // this will hold the animation mixer
const loader = new GLTFLoader();
loader.load('../../assets/deer_walk/scene.gltf', function (gltf) {
deer = gltf.scene;
deer.scale.set(0.01, 0.01, 0.01); // scale the model down
scene.add(deer);
});
Note that the model has no shadow. To fix this, we need to set the castShadow
and receiveShadow
attributes to true
in the model. We can do this by traversing the model and setting the attribute to true
in all the meshes:
loader.load('../../assets/deer_walk/scene.gltf', function (gltf) {
deer = gltf.scene;
deer.scale.set(0.01, 0.01, 0.01);
deer.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(deer);
});
Now we can see the shadow of the deer on the plane.
Now, let's add the animation to the deer:
loader.load('../../assets/deer_walk/scene.gltf', function (gltf) {
deer = gltf.scene;
deer.scale.set(0.01, 0.01, 0.01);
deer.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
mixer = new THREE.AnimationMixer( deer );
const action = mixer.clipAction(gltf.animations[0]); // get the first (and only) animation
action.play();
scene.add(deer);
});
Since the deer has a walking animation, we will need to update the animation in every frame
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
if (mixer) mixer.update(0.01);
}
animate();
index.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(18, 7, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
document.body.appendChild( renderer.domElement );
const axesHelper = new THREE.AxesHelper( 50 );
scene.add( axesHelper );
const controls = new OrbitControls( camera, renderer.domElement );
controls.update();
const ambientLight = new THREE.AmbientLight( 0xffffff, 0.5 );
scene.add( ambientLight );
const spotLight = new THREE.SpotLight( 0xffffff, 0.7 );
spotLight.position.set(2, 12, 2);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.5;
spotLight.decay = 1;
spotLight.distance = 0;
spotLight.castShadow = true;
spotLight.shadow.bias = -0.001;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 60;
spotLight.shadow.focus = 1;
scene.add( spotLight );
scene.add( spotLight.target );
const spotLightHelper = new THREE.SpotLightHelper( spotLight );
scene.add( spotLightHelper );
const planeGeometry = new THREE.PlaneGeometry( 100, 100 );
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xbcbcbc });
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true;
scene.add( plane );
let deer;
let mixer;
const loader = new GLTFLoader();
loader.load('../../assets/deer_walk/scene.gltf', function (gltf) {
deer = gltf.scene;
deer.scale.set(0.01, 0.01, 0.01);
deer.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
mixer = new THREE.AnimationMixer( deer );
const action = mixer.clipAction(gltf.animations[0]); // get the first (and only) animation
action.play();
scene.add(deer);
});
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
if (mixer) mixer.update(0.01);
}
animate();
Update the position of the deer in every frame to make it move:
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
if (mixer) mixer.update(0.01);
if (deer) deer.position.x += 0.01;
}
animate();
The deer will move in the x-axis in every frame. To make it stop when it reaches the end of the plane, add a condition to reset its position:
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
if (mixer) mixer.update(0.01);
if (deer) {
deer.position.x += 0.01;
if (Math.abs(deer.position.x) > 30) {
deer.position.x = 0;
}
}
}
animate();
To make the light follow the deer, set the spot light target to the deer object and update the spot light helper to visualize the light's target:
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
if (mixer) mixer.update(0.01);
if (deer) {
deer.position.x += 0.01;
if (Math.abs(deer.position.x) > 30) {
deer.position.x = 0;
}
spotLight.target = deer;
spotLightHelper.update();
}
}
animate();
Add keyboard controls to rotate the deer:
window.addEventListener('keydown', (event) => {
switch (event.key) {
case 'a':
deer.rotation.y += 0.05;
break;
case 'd':
deer.rotation.y -= 0.05;
break;
}
});
The movement is currently only in the x-axis. To move the deer in the xz plane, use the Math.sin
and Math.cos
functions:
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
if (mixer) mixer.update(0.01);
if (deer) {
deer.position.x += 0.01 * Math.cos(deer.rotation.y);
deer.position.z -= 0.01 * Math.sin(deer.rotation.y);
if (Math.abs(deer.position.x) > 30 || Math.abs(deer.position.z) > 30) {
deer.position.set(0, 0, 0);
}
spotLight.target = deer;
spotLightHelper.update();
}
}
animate();
Note that the z-axis is inverted because the Math.sin
function returns a negative value when the angle is between 180 and 360 degrees. Invert the sign of the z-axis to correct this:
In this tutorial, we learned how to import a 3D model, add lights, shadows, animations, keyboard controls, and move objects in the scene. Feel free to explore further and customize your Three.js projects based on these foundational concepts.
index.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// Scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(18, 7, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
document.body.appendChild( renderer.domElement );
// Helpers
const axesHelper = new THREE.AxesHelper( 50 );
scene.add( axesHelper );
const controls = new OrbitControls( camera, renderer.domElement );
controls.update();
// Lights
const ambientLight = new THREE.AmbientLight( 0xffffff, 0.5 );
scene.add( ambientLight );
const spotLight = new THREE.SpotLight( 0xffffff, 0.7 );
spotLight.position.set(2, 12, 2);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.5;
spotLight.decay = 1;
spotLight.distance = 0;
spotLight.castShadow = true;
spotLight.shadow.bias = -0.001;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 60;
spotLight.shadow.focus = 1;
scene.add( spotLight );
scene.add( spotLight.target );
const spotLightHelper = new THREE.SpotLightHelper( spotLight );
scene.add( spotLightHelper );
// Plane
const planeGeometry = new THREE.PlaneGeometry( 100, 100 );
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xbcbcbc });
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true;
scene.add( plane );
// Importing the deer model
let deer;
let mixer;
const loader = new GLTFLoader();
loader.load('../../assets/deer_walk/scene.gltf', function (gltf) {
deer = gltf.scene;
deer.scale.set(0.01, 0.01, 0.01);
deer.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
mixer = new THREE.AnimationMixer( deer );
const action = mixer.clipAction(gltf.animations[0]); // get the first (and only) animation
action.play();
scene.add(deer);
});
// Animation
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
if (mixer) mixer.update(0.01);
if (deer) {
deer.position.x += 0.01 * Math.cos(deer.rotation.y);
deer.position.z -= 0.01 * Math.sin(deer.rotation.y);
if (Math.abs(deer.position.x) > 30 || Math.abs(deer.position.z) > 30) {
deer.position.set(0, 0, 0);
}
spotLight.target = deer;
spotLightHelper.update();
}
}
animate();
// Keyboard controls
window.addEventListener('keydown', (event) => {
switch (event.key) {
case 'a':
deer.rotation.y += 0.05;
break;
case 'd':
deer.rotation.y -= 0.05;
break;
}
});
For details on the credits and attributions for external resources used in these projects, refer to CREDITS.
This project is licensed under the MIT License © Matheus Silva