Character Movement in Three.js
Implementing character movement involves several key functions, including executing the model's animation, rotating the model, and positioning the model. These functions will be triggered by key presses. Let's implement character movement step by step.
1. Set up
First, set up the necessary environment, including importing the required libraries and initializing the scene, renderer, camera, and controls.
1import * as THREE from "three";
2import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
3import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
4import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
5
6export default class Three {
7 constructor(container) {
8 //VARIABLES
9 this._container = container;
10
11 //SCENE
12 this._scene = new THREE.Scene();
13
14 //RENDERER
15 this._renderer = new THREE.WebGLRenderer({ antialias: true });
16 this._renderer.setSize(
17 this._container.offsetWidth,
18 this._container.offsetHeight
19 );
20 this._renderer.setPixelRatio(window.devicePixelRatio);
21 this._renderer.shadowMap.enabled = true;
22
23 //CAMERA
24 this._camera = new THREE.PerspectiveCamera(
25 75,
26 this._container.offsetWidth / this._container.offsetHeight,
27 0.1,
28 1000
29 );
30 this._camera.position.set(0, 3, 6);
31 this._camera.lookAt(new THREE.Vector3(0, 0, 0));
32 this._container.appendChild(this._renderer.domElement);
33
34 //ORBIT CONTROLS
35 this._orbit = new OrbitControls(this._camera, this._renderer.domElement);
36 this._orbit.enableDamping = true; // smooth movement
37 this._orbit.minDistance = 5;
38 this._orbit.maxDistance = 15;
39 this._orbit.enablePan = false; // block movement using right mouse click
40 this._orbit.maxPolarAngle = Math.PI / 2 - 0.05;
41
42 //CLOCK
43 this._clock = new THREE.Clock();
44
45 //INITIALIZE
46 const grid = new THREE.GridHelper();
47 this._scene.add(grid);
48
49 const animate = this.animate.bind(this);
50 animate();
51 }
52
53 animate() {
54 requestAnimationFrame(this.animate.bind(this));
55 if (this._orbit && this._orbit.enabled) this._orbit.update();
56 this._renderer.render(this._scene, this._camera);
57 }
58}
2. Add Event Listeners for Key Press
Next, add key event listeners to handle character movement based on key presses.
1constructor() {
2 this._keyMap = new Map();
3}
4
5setKeyEvent() {
6 window.addEventListener("keydown", (e) => {
7 if (e.code === "KeyW") this._keyMap.set("w", true);
8 if (e.code === "KeyA") this._keyMap.set("a", true);
9 if (e.code === "KeyS") this._keyMap.set("s", true);
10 if (e.code === "KeyD") this._keyMap.set("d", true);
11 if (e.code === "ShiftLeft") this._keyMap.set("shift", true);
12 });
13 window.addEventListener("keyup", (e) => {
14 if (e.code === "KeyW") this._keyMap.set("w", false);
15 if (e.code === "KeyA") this._keyMap.set("a", false);
16 if (e.code === "KeyS") this._keyMap.set("s", false);
17 if (e.code === "KeyD") this._keyMap.set("d", false);
18 if (e.code === "ShiftLeft") this._keyMap.set("shift", false);
19 });
20 }
3. Create Class for Control Model
Define a class to manage the model's animation, rotation, and positioning based on the key presses.
1) Animation
Determine the appropriate animation state based on the key presses.
1let isDirKeyPressed = false;
2
3for (const entry of keyMap) {
4 if (entry[0] !== "shift" && entry[1]) {
5 isDirKeyPressed = true;
6 break;
7 }
8}
9
10let nextActionName;
11
12
13if (isDirKeyPressed && keyMap.get("shift")) nextActionName = "Run";
14else if (isDirKeyPressed) nextActionName = "Walk";
15else nextActionName = "Idle";
16
17if (this.curActionName !== nextActionName) {
18 const curAction = this.animationMap.get(this.curActionName);
19 const nextAction = this.animationMap.get(nextActionName);
20
21 curAction.fadeOut(this.fadeDuration);
22 nextAction.reset().fadeIn(this.fadeDuration).play();
23 this.curActionName = nextActionName;
24}
25
26this.mixer.update(delta);
2) Rotation
Rotate the model based on the direction keys pressed.
1if (isDirKeyPressed) {
2
3 let sightAngleInZXPlane = Math.atan2( // get θ1
4 this.camera.position.x - this.model.position.x,
5 this.camera.position.z - this.model.position.z
6 );
7
8 const offsetAngle = this.getOffsetAngle(keyMap); // get θ2
9 const rotateQuarternion = new THREE.Quaternion();
10
11 rotateQuarternion.setFromAxisAngle( // get final angle
12 new THREE.Vector3(0, 1, 0),
13 sightAngleInZXPlane + offsetAngle
14 );
15 this.model.quaternion.rotateTowards(rotateQuarternion, 0.2);
16}
17
18
1getOffsetAngle(keyMap) {
2 let offsetAngle = 0;
3
4 if (keyMap.get("w")) {
5 if (keyMap.get("a")) offsetAngle = Math.PI / 4;
6 else if (keyMap.get("d")) offsetAngle = -Math.PI / 4;
7 } else if (keyMap.get("s")) {
8 if (keyMap.get("a")) offsetAngle = Math.PI * (3 / 4);
9 else if (keyMap.get("d")) offsetAngle = -Math.PI * (3 / 4);
10 else offsetAngle = Math.PI;
11 } else if (keyMap.get("a")) {
12 offsetAngle = Math.PI / 2;
13 } else if (keyMap.get("d")) {
14 offsetAngle = -Math.PI / 2;
15 }
16
17 return offsetAngle;
18}
3) Position
Update the model's position based on the direction keys pressed.
1if (isDirKeyPressed) {
2
3
4 const direction = new THREE.Vector3();
5 const velocity = keyMap.get("shift") ? 0.03 : 0.016;
6
7
8 this.camera.getWorldDirection(direction);
9 direction.y = 0;
10 direction.normalize();
11 direction.applyAxisAngle(new THREE.Vector3(0, 1, 0), offsetAngle);
12
13 const movement = direction.clone().multiplyScalar(velocity);
14 this.model.position.add(movement);
15 this.camera.position.add(movement);
16}
4. Orbit Control
Update the orbit control target to follow the model.
1if (isDirKeyPressed) {
2 if (this.orbit.enabled) {
3 this.orbit.target = new THREE.Vector3().copy(this.model.position);
4 }
5}
4. Load Model
Load the 3D model and set up the necessary environment and animations.
1import ModelControl from "./control";
2
3loadModel() {
4 //SET ENV LIGHT
5 const rLoader = new RGBELoader();
6 rLoader.load("/HDR/MR_INT-004_BigWindowTree_Thea.hdr", (texture) => {
7 texture.mapping = THREE.EquirectangularReflectionMapping;
8 this._scene.environment = texture;
9 });
10
11 //LOAD MODEL
12 const loader = new GLTFLoader();
13 loader.load("3d/soldier/soldier.glb", (gltf) => {
14 const model = gltf.scene;
15 const mixer = new THREE.AnimationMixer(model);
16 const animationMap = gltf.animations.reduce((acc, cur) => {
17 const action = mixer.clipAction(cur);
18 acc.set(cur.name, action);
19 return acc;
20 }, new Map());
21
22 this._scene.add(model);
23 this._control = new ModelControl(
24 model,
25 mixer,
26 animationMap,
27 "Idle",
28 this._camera,
29 this._orbit
30 );
31 });
32 }