How to Make a Model's Head Follow the Cursor in Three.js

Published on 29 Nov, 2024 | ~5 min read | Demo

In this article, we’ll bring together what we learned from Animating Models Programmatically in Three.js and Interactive Object Creation in Three.js with Mouse Clicks to build the interactive model animation featured in the demo.

Targeting the Head Bone

So, first things first, set up a Three.js project and copy the code below. Oh, and if you want to save time, I’ve got a Three.js boilerplate you can clone and start using right away.

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.setClearColor(0xffffff);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  45,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

camera.position.set(0, 1, 2.5);
camera.lookAt(scene.position);

const light = new THREE.DirectionalLight(0xffffff, 10);
scene.add(light);
light.position.y = 20;
light.position.z = 20;

const aLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(aLight);

const loader = new GLTFLoader();
loader.load('/character_male_sci-fi.glb', function (glb) {
  const model = glb.scene;
  scene.add(model);
  model.position.y -= 1;
});

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

window.addEventListener('resize', function () {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

Next, we’ll reuse the same snippet we used for creating objects on a mouse click. This time, though, we’ll introduce a seventh variable, to which we’ll assign the head bone.

Also, we’ll replace the click event with the mousemove event, allowing the head to follow the cursor as it moves rather than only when you click (unless you prefer that, of course).

let head;

const intersectionPoint = new THREE.Vector3();
const planeNormal = new THREE.Vector3();
const plane = new THREE.Plane();
const mousePosition = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
window.addEventListener('mousemove', function (e) {
  mousePosition.x = (e.clientX / this.window.innerWidth) * 2 - 1;
  mousePosition.y = -(e.clientY / this.window.innerHeight) * 2 + 1;
  planeNormal.copy(camera.position).normalize();
  plane.setFromNormalAndCoplanarPoint(planeNormal, scene.position);
  raycaster.setFromCamera(mousePosition, camera);
  raycaster.ray.intersectPlane(plane, intersectionPoint);

  if (head) head.lookAt(intersectionPoint.x, intersectionPoint.y, 2);
});
loader.load('/character_male_sci-fi.glb', function (glb) {
  const model = glb.scene;
  scene.add(model);
  model.position.y -= 1;

  head = model.getObjectByName('Head_3');
});

As you can see in the last line, we use the lookAt() method to keep the head oriented toward the intersection point, which represents the current position of the cursor.

An important thing to keep in mind is to set a specific value for the z coordinate. Using the value from the intersection point could cause issues if it turns out to be negative.

Taking the z value from the intersection point
Taking the z value from the intersection point

Targeting Other Bones

Now that we’ve wrapped up the main objective of this tutorial, let’s add some extra details.

That said, let's create a breathing animation.

To do that, we’ll target the Spine_55 bone with a subtle animation.

We’ll assign the bone to a global variable and apply a slight translation to it in the animate() function.

let head, torso;
torso = model.getObjectByName('Spine_55');
function animate(time) {
  if (head) {
    torso.position.y = 0.003 * Math.sin(time / 800);
  }
  renderer.render(scene, camera);
}

The animation might not be super noticeable, but that’s how it is in real life, after all.

Moving on, we’ll rotate the arms, forearms, and fingers using their corresponding bones to shape a more natural pose, replacing the T-pose.

loader.load('/character_male_sci-fi.glb', function (glb) {
  const model = glb.scene;
  scene.add(model);
  model.position.y -= 1;

  head = model.getObjectByName('Head_3');
  torso = model.getObjectByName('Spine_55');

  const rArm = model.getObjectByName('RightArm_51');
  const rForeArm = model.getObjectByName('RightForeArm_50');
  rArm.rotation.x = 1.3;
  rForeArm.rotation.x = 0.3;

  const rHandThumb1 = model.getObjectByName('RightHandThumb1_32');
  const rHandThumb2 = model.getObjectByName('RightHandThumb2_31');
  const rHandThumb3 = model.getObjectByName('RightHandThumb3_30');
  rHandThumb1.rotation.x = -0.2;
  rHandThumb2.rotation.x = 0.3;
  rHandThumb3.rotation.x = 0.3;

  const rHandIndex1 = model.getObjectByName('RightHandIndex1_36');
  const rHandIndex2 = model.getObjectByName('RightHandIndex2_35');
  const rHandIndex3 = model.getObjectByName('RightHandIndex3_34');
  rHandIndex1.rotation.x = 0.2;
  rHandIndex2.rotation.x = 0.3;
  rHandIndex3.rotation.x = 0.3;

  const rHandMiddle1 = model.getObjectByName('RightHandMiddle1_40');
  const rHandMiddle2 = model.getObjectByName('RightHandMiddle2_39');
  const rHandMiddle3 = model.getObjectByName('RightHandMiddle3_38');
  rHandMiddle1.rotation.x = 0.2;
  rHandMiddle2.rotation.x = 0.3;
  rHandMiddle3.rotation.x = 0.3;

  const rHandRing1 = model.getObjectByName('RightHandRing1_44');
  const rHandRing2 = model.getObjectByName('RightHandRing2_43');
  const rHandRing3 = model.getObjectByName('RightHandRing3_42');
  rHandRing1.rotation.x = 0.2;
  rHandRing2.rotation.x = 0.3;
  rHandRing3.rotation.x = 0.3;

  const rHandPinky1 = model.getObjectByName('RightHandPinky1_48');
  const rHandPinky2 = model.getObjectByName('RightHandPinky2_47');
  const rHandPinky3 = model.getObjectByName('RightHandPinky3_46');
  rHandPinky1.rotation.x = 0.2;
  rHandPinky2.rotation.x = 0.3;
  rHandPinky3.rotation.x = 0.3;

  const lArm = model.getObjectByName('LeftArm_27');
  const lForeArm = model.getObjectByName('LeftForeArm_26');
  lArm.rotation.x = 1.3;
  lForeArm.rotation.x = 0.3;

  const lHandThumb1 = model.getObjectByName('LeftHandThumb1_8');
  const lHandThumb2 = model.getObjectByName('LeftHandThumb2_7');
  const lHandThumb3 = model.getObjectByName('LeftHandThumb3_6');
  lHandThumb1.rotation.x = -0.2;
  lHandThumb1.rotation.z = 0.5;
  lHandThumb2.rotation.x = 0.3;
  lHandThumb3.rotation.x = 0.3;

  const lHandIndex1 = model.getObjectByName('LeftHandIndex1_12');
  const lHandIndex2 = model.getObjectByName('LeftHandIndex2_11');
  const lHandIndex3 = model.getObjectByName('LeftHandIndex3_10');
  lHandIndex1.rotation.x = 0.2;
  lHandIndex2.rotation.x = 0.3;
  lHandIndex3.rotation.x = 0.3;

  const lHandMiddle1 = model.getObjectByName('LeftHandMiddle1_16');
  const lHandMiddle2 = model.getObjectByName('LeftHandMiddle2_15');
  const lHandMiddle3 = model.getObjectByName('LeftHandMiddle3_14');
  lHandMiddle1.rotation.x = 0.2;
  lHandMiddle2.rotation.x = 0.3;
  lHandMiddle3.rotation.x = 0.3;

  const lHandRing1 = model.getObjectByName('LeftHandRing1_20');
  const lHandRing2 = model.getObjectByName('LeftHandRing2_19');
  const lHandRing3 = model.getObjectByName('LeftHandRing3_18');
  lHandRing1.rotation.x = 0.2;
  lHandRing2.rotation.x = 0.3;
  lHandRing3.rotation.x = 0.3;

  const lHandPinky1 = model.getObjectByName('LeftHandPinky1_24');
  const lHandPinky2 = model.getObjectByName('LeftHandPinky2_23');
  const lHandPinky3 = model.getObjectByName('LeftHandPinky3_22');
  lHandPinky1.rotation.x = 0.2;
  lHandPinky2.rotation.x = 0.3;
  lHandPinky3.rotation.x = 0.3;
});

Last Words

And there you go! That’s another awesome trick to add to your Three.js toolbox.

Happy coding!

Buy me a coffee

Credits and Resources

Related Content