I’m working on a Three.js project where I need a third-person camera setup that follows a 3D model as it moves around the scene. I have a basic implementation with animations and controls, but I’m struggling to make the camera follow the model properly. Here’s a summary of what I’m trying to achieve and the current issues I’m facing:
Problem Summary:
I want the camera to follow the model from a third-person perspective. Specifically, as the model moves in different directions (forward, backward, left, right), the camera should always follow the model while maintaining a fixed distance and orientation.
Current Implementation:
<script setup>
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { onMounted, ref } from 'vue';
const container3D = ref(null);
onMounted(() => {
let object, mixer, walkAction, shutdownAction, CycleBackAction, jumpAction;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
const cameraOffset = new THREE.Vector3(0, 30, -50);
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 5000);
scene.add(camera);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
container3D.value.appendChild(renderer.domElement);
const loader = new GLTFLoader();
loader.load(
'/medium_mech_striker/scene.gltf',
(gltf) => {
object = gltf.scene;
object.scale.set(10, 10, 10);
object.position.y = -200;
object.rotateY(Math.PI);
object.frustumCulled = false;
scene.add(object);
// Setup animations
mixer = new THREE.AnimationMixer(object);
gltf.animations.forEach((clip) => {
if (clip.name === 'a5WalkCycle') {
walkAction = mixer.clipAction(clip);
walkAction.setEffectiveTimeScale(1); // Adjust this value to control the speed of the walk animation
} else if (clip.name === 'a1ShutdownPose') {
shutdownAction = mixer.clipAction(clip);
} else if (clip.name === 'a6WalkCycleBack') {
CycleBackAction = mixer.clipAction(clip);
} else if (clip.name === 'a8Jump') {
jumpAction = mixer.clipAction(clip);
}
});
// Start with shutdown animation
if (shutdownAction) {
shutdownAction.play();
}
},
undefined,
(error) => {
console.error('An error happened while loading the GLTF model:', error);
}
);
// Create plane mesh
const planeGeometry = new THREE.PlaneGeometry(10000, 10000);
planeGeometry.rotateX(-Math.PI / 2);
const planeMaterial = new THREE.ShadowMaterial({ color: 0x000000, opacity: 0.2 });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.receiveShadow = true;
scene.add(plane);
const GridHelper = new THREE.GridHelper(2000, 100);
GridHelper.position.y = -199;
GridHelper.material.opacity = 0.25;
GridHelper.material.transparent = true;
scene.add(GridHelper);
// Light
scene.add(new THREE.AmbientLight(0xf0f0f0, 3));
const light = new THREE.SpotLight(0xffffff, 4.5);
light.position.set(0, 1500, 200);
light.angle = Math.PI * 0.2;
light.decay = 0;
light.castShadow = true;
light.shadow.camera.near = 200;
light.shadow.camera.far = 2000;
light.shadow.bias = -0.000222;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
scene.add(light);
// Orbit controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.screenSpacePanning = false;
// Handle keyboard input
const keys = {};
window.addEventListener('keydown', (event) => {
keys[event.key] = true;
});
window.addEventListener('keyup', (event) => {
keys[event.key] = false;
});
// Animation function
function animate() {
requestAnimationFrame(animate);
// Update mixer if defined
if (mixer) {
mixer.update(0.03); // Adjust this value to control the overall animation update speed
}
// Handle animations
if (object) {
if (keys['w']) {
if (shutdownAction && shutdownAction.isRunning()) {
shutdownAction.stop();
}
if (walkAction && !walkAction.isRunning()) {
walkAction.play();
}
if (keys['d']) {
object.rotation.y += 0.05;
} else if (keys['a']) {
object.rotation.y -= 0.05;
} else if (keys[' ']) {
if (!jumpAction.isRunning()) jumpAction.play();
} else {
if (jumpAction.isRunning()) jumpAction.stop();
}
const direction = new THREE.Vector3();
object.getWorldDirection(direction);
object.position.addScaledVector(direction, 1);
} else if (keys['s']) {
if (walkAction && walkAction.isRunning()) {
walkAction.stop();
}
if (CycleBackAction && !CycleBackAction.isRunning()) {
CycleBackAction.play();
}
const direction = new THREE.Vector3();
object.getWorldDirection(direction);
object.position.addScaledVector(direction, -1);
} else {
if (walkAction && walkAction.isRunning()) {
walkAction.stop();
}
if (shutdownAction && !shutdownAction.isRunning()) {
shutdownAction.play();
}
if (CycleBackAction && CycleBackAction.isRunning()) {
CycleBackAction.stop();
}
}
}
// Update camera position to follow the object
const objectPosition = new THREE.Vector3();
object.getWorldPosition(objectPosition);
// Calculate camera position based on object position and offset
const offset = cameraOffset.clone();
offset.applyMatrix4(object.matrixWorld);
camera.position.copy(objectPosition).add(offset);
// Ensure camera always faces the object
camera.lookAt(objectPosition);
controls.update();
renderer.render(scene, camera);
}
// Start animation
animate();
// Adjust camera and renderer size when window is resized
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
});
</script>
Issue:
The camera currently doesn’t follow the model’s movements. The camera remains static and doesn’t adjust its position or orientation based on the model’s position or rotation.
What I’ve Tried:
I have set up the camera and renderer properly.
I’ve implemented animation controls for moving and rotating the model.
I’ve used OrbitControls to manage the camera view.
Questions:
How can I modify the camera setup so that it follows the model from a third-person perspective?
What changes are needed in the code to ensure that the camera maintains a fixed distance from the model while it moves around?
Any guidance or suggestions would be greatly appreciated. Thank you!