Skip to content

Brief overview of Three.js, a WebGL-based 3D library crucial for delving into computer graphics studies.

License

Notifications You must be signed in to change notification settings

math-silva/threejs-introduction

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Three.js Introduction

threejs-introduction

Table of Contents

Author

Introduction

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.

Requirements

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.

For Node.js Users

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

For Python Users

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.

How to Run

1. Clone the Repo

git clone https://github.com/math-silva/threejs-introduction.git

2. Navigate to the Project Folder

cd threejs-introduction

3. Run the Web Server

3.1. Using npm (Node.js)

http-server

3.2. Using Python

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.

Tutorial

1. Importing Three.js

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.

2. Setting up the scene

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();

Helper 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();

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

3. Adding lights

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 );

Light Scene

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 );

Light Scene

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();

4. Importing a 3D model

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.

Deer walk

4.1. Importing the model

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);
});

Deer Scene

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);
});

Deer with shadow

Now we can see the shadow of the deer on the plane.

4.2. Adding animations

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();

Deer with animation

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();

5. Adding mechanics

5.1. Moving the deer

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();

Deer moving

5.2. Spotting the deer with the light

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();

Deer moving with lights

5.3. Adding keyboard controls

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:

Deer moving with keyboard controls

6. Conclusion

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;
  }
});

Credits

For details on the credits and attributions for external resources used in these projects, refer to CREDITS.

License

This project is licensed under the MIT License © Matheus Silva

About

Brief overview of Three.js, a WebGL-based 3D library crucial for delving into computer graphics studies.

Topics

Resources

License

Stars

Watchers

Forks