Recap
In the previous article, we explored how to design your own Avatar for ThreeJS Metaverse with Animations.

- In this article, we will import this
avatar.glbto our ThreeJS project. - Boiler Plate for this article is in GitHub branch if you have been following along then please use your own asset.

- Proceed to modify the following.
index.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>DONTKILLME METAVERSE</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<canvas class="threejs"></canvas>
<script type="module" src="./index.js"></script>
<div class="overlay"></div>
<h1 class="loading">
Loading Experience... <span id="progressPercentage"></span>%
</h1>
<button class="start">START</button>
</body>
</html>Camera.js
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { sizesStore } from "./Utils/Store.js";
import App from "./App.js";
export default class Camera {
constructor() {
this.app = new App();
this.canvas = this.app.canvas;
this.sizesStore = sizesStore;
this.sizes = this.sizesStore.getState();
this.setInstance();
this.setControls();
this.setResizeListner();
}
setInstance() {
this.instance = new THREE.PerspectiveCamera(
35,
this.sizes.width / this.sizes.height,
1,
600
);
this.instance.position.z = 100;
this.instance.position.y = 20;
}
setControls() {
this.controls = new OrbitControls(this.instance, this.canvas);
this.controls.enableDamping = true;
}
setResizeListner() {
this.sizesStore.subscribe((sizes) => {
this.instance.aspect = sizes.width / sizes.height;
this.instance.updateProjectionMatrix();
});
}
loop() {
this.controls.update();
this.characterController = this.app.world.characterController?.rigidBody;
if (this.characterController) {
const characterRotation = this.characterController.rotation();
const characterPosition = this.characterController.translation();
const cameraOffset = new THREE.Vector3(0, 5, 20);
cameraOffset.applyQuaternion(characterRotation);
cameraOffset.add(characterPosition);
const targetOffset = new THREE.Vector3(0, 2, 0);
targetOffset.applyQuaternion(characterRotation);
targetOffset.add(characterPosition);
this.instance.position.lerp(cameraOffset, 0.1);
this.controls.target.lerp(targetOffset, 0.1);
}
}
}Renderer.js
import * as THREE from "three";
import App from "./App";
import { sizesStore } from "./Utils/Store";
export default class Renderer {
constructor() {
console.log("Renderer Init");
this.app = new App();
this.canvas = this.app.canvas;
this.camera = this.app.camera;
this.scene = this.app.scene;
// Code to resize
this.sizesStore = sizesStore;
this.sizes = this.sizesStore.getState();
this.setInstance();
this.setResizeListner();
}
setInstance() {
console.log("Renderer setInstance Called");
this.instance = new THREE.WebGLRenderer({
canvas: this.canvas,
antialias: true,
});
this.instance.setSize(this.sizes.width, this.sizes.height);
this.instance.setPixelRatio(Math.min(this.sizes.pixelRatio, 2));
this.instance.outputEncoding = THREE.sRGBEncoding;
}
setResizeListner() {
this.sizesStore.subscribe((sizes) => {
this.instance.setSize(sizes.width, sizes.height);
this.instance.setPixelRatio(sizes.pixelRatio);
});
}
loop() {
this.instance.render(this.scene, this.camera.instance);
}
}Preloader.js
import assetStore from "../Utils/AssetStore";
import { appStateStore } from "../Utils/Store";
export default class Preloader {
constructor() {
this.assetStore = assetStore;
// Hiding the Screen before everything gets loaded
this.overlay = document.querySelector(".overlay");
this.loading = document.querySelector(".loading");
this.startButton = document.querySelector(".start");
this.assetStore.subscribe((state) => {
// console.log("STATE", state.loadedAssets);
this.numberOfLoadedAssets = Object.keys(state.loadedAssets).length;
this.numberOfAssetsToLoad = state.assetsToLoad.length;
this.progress = this.numberOfLoadedAssets / this.numberOfAssetsToLoad;
// console.log("progress", this.progress);
document.getElementById("progressPercentage").innerHTML = Math.trunc(
this.progress * 100
);
if (this.progress === 1) {
appStateStore.setState({ assetsReady: true });
this.loading.classList.add("fade");
window.setTimeout(() => this.ready(), 1200);
}
});
}
ready() {
// remove the loading elelemnt from DOM
this.loading.remove();
this.startButton.style.display = "inline";
this.startButton.classList.add("fadeIn");
this.startButton.addEventListener(
"click",
() => {
this.overlay.classList.add("fade");
this.startButton.classList.add("fadeOut");
// remove the overlay and startButton elelemnt from DOM
window.setTimeout(() => {
this.overlay.remove();
this.startButton.remove();
}, 2000);
},
{ once: true }
);
}
}AssetStore.js
import { createStore } from "zustand/vanilla";
const assetsToLoad = [
{
id: "avatar",
path: "/models/avatar.glb",
type: "model",
},
];
// addLoad function is just adding assets to loadedAssets object once they are loaded
const assetStore = createStore((set) => ({
assetsToLoad,
loadedAssets: {},
addLoadedAsset: (asset, id) =>
set((state) => ({
loadedAssets: {
...state.loadedAssets,
[id]: asset,
},
})),
}));
export default assetStore;Store.js
import { createStore } from "zustand";
export const sizesStore = createStore(() => ({
width: window.innerWidth,
height: window.innerHeight,
pixelRatio: Math.min(window.devicePixelRatio, 2),
}));
export const appStateStore = createStore(() => ({
physicsReady: false,
assetsReady: false,
}));
export const inputStore = createStore(() => ({
forward: false,
backward: false,
left: false,
right: false,
}));AnimationController.js
import * as THREE from 'three';
import App from '../App';
export default class AnimationController {
constructor() {
this.app = new App();
this.scene = this.app.scene;
this.avatar = this.app.world.character.avatar;
}
}Character.js
import * as THREE from "three";
import assetStore from "../Utils/AssetStore.js";
import App from "../App.js";
export default class Character {
constructor() {
this.app = new App();
this.scene = this.app.scene;
this.assetStore = assetStore.getState();
this.avatar = this.assetStore.loadedAssets.avatar;
this.instantiateCharacter();
}
instantiateCharacter() {
// create character and add to scene
const geometry = new THREE.BoxGeometry(2, 5, 2);
const material = new THREE.MeshStandardMaterial({
color: 0x00ff00,
wireframe: true,
visible: false,
});
this.instance = new THREE.Mesh(geometry, material);
this.instance.position.set(0, 4, 0);
this.scene.add(this.instance);
// add avatar to character
const avatar = this.avatar.scene;
avatar.rotation.y = Math.PI;
avatar.position.y = -2.5;
avatar.scale.setScalar(3);
this.instance.add(avatar);
}
}CharacterController.js
// Import necessary modules
import * as THREE from "three";
import App from "../App.js";
import { inputStore } from "../Utils/Store.js";
/**
* Class representing a character controller.
*/
export default class CharacterController {
/**
* Create a character controller.
*/
constructor() {
// Initialize app, scene, physics, and character properties
this.app = new App();
this.scene = this.app.scene;
this.physics = this.app.world.physics;
this.character = this.app.world.character.instance;
// Subscribe to input store and update movement values
inputStore.subscribe((state) => {
this.forward = state.forward;
this.backward = state.backward;
this.left = state.left;
this.right = state.right;
});
// Instantiate controller and create rigid body and collider
this.instantiateController();
}
/**
* Instantiate the character controller, rigid body, and collider.
*/
instantiateController() {
// Create a kinematic rigid body
this.rigidBodyType =
this.physics.rapier.RigidBodyDesc.kinematicPositionBased();
this.rigidBody = this.physics.world.createRigidBody(this.rigidBodyType);
// Create a cuboid collider
this.colliderType = this.physics.rapier.ColliderDesc.cuboid(1, 2.5, 1);
this.collider = this.physics.world.createCollider(
this.colliderType,
this.rigidBody
);
// Set rigid body position to character position
const worldPosition = this.character.getWorldPosition(new THREE.Vector3());
const worldRotation = this.character.getWorldQuaternion(
new THREE.Quaternion()
);
this.rigidBody.setTranslation(worldPosition);
this.rigidBody.setRotation(worldRotation);
// Create character controller, set properties, and enable autostepping
this.characterController =
this.physics.world.createCharacterController(0.01);
this.characterController.setApplyImpulsesToDynamicBodies(true);
this.characterController.enableAutostep(5, 0.1, false);
this.characterController.enableSnapToGround(1);
}
/**
* Loop function that updates the character's position and movement.
*/
loop() {
// Initialize movement vector based on input values
const movement = new THREE.Vector3();
if (this.forward) {
movement.z -= 1;
}
if (this.backward) {
movement.z += 1;
}
if (this.left) {
movement.x -= 1;
}
if (this.right) {
movement.x += 1;
}
// Rotate character based on movement vector
if (movement.length() !== 0) {
const angle = Math.atan2(movement.x, movement.z) + Math.PI;
const characterRotation = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
angle
);
this.character.quaternion.slerp(characterRotation, 0.1);
}
// Normalize and scale movement vector and set y component to -1
movement.normalize().multiplyScalar(0.3);
movement.y = -1;
// Update collider movement and get new position of rigid body
this.characterController.computeColliderMovement(this.collider, movement);
const newPosition = new THREE.Vector3()
.copy(this.rigidBody.translation())
.add(this.characterController.computedMovement());
// Set next kinematic translation of rigid body and update character position
this.rigidBody.setNextKinematicTranslation(newPosition);
this.character.position.lerp(this.rigidBody.translation(), 0.1);
}
}World.js
import * as THREE from "three";
import App from "../App.js";
import Physics from "./Physics.js";
import Environment from "./Environment.js";
import Character from "./Character.js";
import CharacterController from "./CharacterController.js";
import AnimationController from "./AnimationController.js";
import { appStateStore } from "../Utils/Store.js";
export default class World {
constructor() {
this.app = new App();
this.scene = this.app.scene;
this.physics = new Physics();
// create world classes
const unsub = appStateStore.subscribe((state) => {
if (state.physicsReady && state.assetsReady) {
this.environment = new Environment();
this.character = new Character();
this.characterController = new CharacterController();
this.animationController = new AnimationController();
unsub();
}
});
this.loop();
}
loop(deltaTime, elapsedTime) {
this.physics.loop();
if (this.characterController) this.characterController.loop();
}
}Run your App

- We removed the commented-out loading screen, as models now need to load during the initial loading phase.
- Updated the
charactervariable tocharacterControllerin thecamera.jsfile. - Added
sRGBEncodingtoRenderer.jsto improveoutputEncodingefficiency. - Set
assetsReadytotruein thePreloaderfile once loading is complete. - Specified the models' path in
AssetStore.js. - Introduced a new
assetsReadykey in theappStateStore. - Created an
animationControllerfile for use in the upcoming lesson. - Split the old
characterfile into two separate files:characterfor definingmeshandavatar, andcharacterControllerfor character control functions. - Updated
world.jsto importcharacterControllerandanimationController, subscribing to them when both physics and assets are ready.
You can review the above description as you compare it with the changes in your VSCode.

- You can push your character for a pull request. I'd love to see how it looks and interacts with it!
- Compare the final code from this GitHub branch.
I hope this article has given you valuable insights and a solid starting point for your upcoming projects. If you found it helpful, consider showing your support by clapping and starring the GitHub repo. Your involvement in this learning journey truly means a lot!
Integrate Animations

Are you ready to elevate your Three.js skills? I've put together a series of articles that break down its core concepts. Initially crafted for my own learning, I'm thrilled to now share these resources with you. Happy exploring!