Third person controller in Three.js

Oslaw Dev
5 min readNov 29, 2021

--

Final result

Live demo: https://webgl-third-person-controller.vercel.app/

Controls

  • W — Move forwards
  • S — Move backwards
  • A — Turn left
  • D — Turn Right
  • Shift + W — Run forward 🚀

To follow this walkthrough it is required to have basic knowledge of javascript and Three.js. This is not a Three.js tutorial, I will focus on the character control mechanic itself.

Three.js official documentation is really good and contains a lot of useful examples I highly recommend to read.

  1. Preparations

First things first, we are going to load a model and add it to the scene, then set perspective camera to lookAt the character. Also we need to extract animations and assign them to the action object, which we are going to use later to animate our character. For the character I used a free model, downloaded from Mixamo.

Animations were merged and converted to gltf format in Blender, I recommend to follow this tutorial: https://www.donmccurdy.com/2017/11/06/creating-animated-gltf-characters-with-mixamo-and-blender/

Default animation is set to idle and camera is looking at the model. Good start.

Me when I finished this block of code

2. Character control

Perhaps the simplest part. The process is pretty straightforward. We need to add eventListener on press key events and translate character along Z axis.

By default we set velocity to zero: let velocity = 0.0, this is a state of idle. And create variable for speed: let speed = 0;

After a number of experimentations I decided to set speed to 0.09 for walking and 0.2 for running, and the negative values for moving backwards. Feel free to tweak params as desired to achieve preferable speed of movement.

Now we need to add the equation:

velocity += (speed — velocity) * 0.3

Wait, what? Why do we need velocity?

We could just apply speed directly to the character and call it a day. But I want the character to gain velocity gradually to simulate real life human walking momentum.

On each frame (tick) we calculate velocity applied to the character. On the first frame velocity equals zero, so the next velocity value will be (0.2–0) * 0.3 = 0.06. On the next frame velocity is 0.06 so we solve the equation again using this as a new value velocity += (0.2–0.06) * 0.3 = 0.102. It keeps increasing until the velocity is equal (or aprox equal) to the speed (0.2–0.2) * 0.3 = 0. At this point the velocity remains the same — the character has gained the momentum and moves with a desired speed. Offset number controls how fast the character will gain that speed.

Finally, to turn the character we simply apply rotation to the model.

Result:

Moving the character around

3. Camera movement

Now is the trickiest part. We want camera to follow the model, but we don’t want it to be firmly glued right behind the character’s back.

Let’s take a look at this scheme:

The logic behind smooth camera movements

We will create two helper objects Follower and Tail. The Tail will be always placed on a fixed distance behind the character and Follower will smoothly lerp to Tail position. The camera will be added to the Follower object to copy its path.

Just for the experimentation sake, I will create visible 3D objects to demonstrate how it works

As you noticed we also created four vectors. Each vector will represent a position of each object related to the character movement: character position, follower position, tail position and finally camera offset. Why having so many vectors?

Let’s try to just lerp Follower object (camera) to the tail:

The Follower chooses the shortest path to lerp to the tail and this is not quite what we want. What we do want though is to maintain the stable distance from the character. For that matter we are going to introduce camera_offset vector to scale up the Vector3 of the Follower.

Looks a little bit frustrating, doesn’t it?

The logic is pretty simple though, all we need to do is to compensate the distance by scaling the Follower vector up with camera_offset vector.

Let’s take a look at this:

camera_offset.copy( character_position ).sub( camera_position ).normalize();

  1. We take the defined earlier camera_ofset Vector3 which is by default (0,0,0) and copy character position on each tick
  2. Then we subtract camera position vector from character position vector just like in the picture below:
Vector subtraction visualised

Vector subtraction: https://www.dummies.com/education/science/physics/how-to-subtract-vectors/#:~:text=To%20subtract%20two%20vectors%2C%20you,you're%20subtracting%20it%20from.

Then we count the distance we need to compensate:

const distanceDifference = character_position.distanceTo( camera_position ) — distance;

Now we need to multiply camera_offset vector on the distanceDifference and add result to the follower vector to adjust Follower position. For that we can use addScaledVector method from Three.js.

follower.position.addScaledVector( camera_offset, distanceDifference );

Finally we can lerp

follower.position.lerp(tail_position, 0.02);

To read:

Lerp: https://threejs.org/docs/#api/en/math/Vector3.lerp

Copy: https://threejs.org/docs/#api/en/math/Vector3.copy

Absolute position: https://threejs.org/docs/#api/en/math/Vector3.setFromMatrixPosition

Result:

Let’s remove the cubes and leave just empty 3dObjects instead. And assign them to Tail and Follower.

let tail = new THREE.Object3D;

let follower = new THREE.Object3D;

And add camera to the follower.

follower.add(camera)

Next step is animation!

4. Animation

Back to simple stuff.

We want to have a smooth transition between animations. To achieve it, we are going to create a cross fade animation function, where we switch between animations in three steps:

  1. Reset animation to play from 0 frame
  2. Play new animation
  3. Apply crossfade from previous animation for a smooth transition

function crossfadeAnimation(newAction){

newAction.reset()

newAction.play()

newAction.crossFadeFrom(action[prevAnim], 0.3)

}

To apply crossfade we need to store somewhere the previously played animation. Remember the prevAnim variable? This is it.

🎉 Congratulations, we are done!

See the full result here: https://github.com/oslavdev/webgl-third-person-controller

In the next articles we will add a preloader, touchpad controller and physics.

--

--