brandgrandprix / client /src /camera.js
Pushkar Pandey
Fix camera position to be behind the car instead of in front, fixing reversed controls
4d6f475
Raw
History Blame Contribute Delete
3.8 kB
import * as THREE from 'three';
const OFFSET_BEHIND = 10;
const OFFSET_ABOVE = 4.5;
const LOOK_AHEAD = 6;
const LERP_POS = 0.1;
const LERP_LOOK = 0.08;
export class ChaseCamera {
constructor(camera) {
this.camera = camera;
this.targetPos = new THREE.Vector3();
this.targetLook = new THREE.Vector3();
this._tmpPos = new THREE.Vector3();
this._tmpLook = new THREE.Vector3();
// 360-degree free-look variables
this.orbitYaw = 0; // horizontal rotation offset
this.orbitPitch = 0; // vertical rotation offset
this.isDragging = false;
this.lastMouseX = 0;
this.lastMouseY = 0;
this._bindEvents();
}
_bindEvents() {
window.addEventListener('mousedown', (e) => {
this.isDragging = true;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
});
window.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
const dx = e.clientX - this.lastMouseX;
const dy = e.clientY - this.lastMouseY;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
this.orbitYaw += dx * 0.01;
this.orbitPitch += dy * 0.01;
// Clamp pitch so they don't look upside down
this.orbitPitch = Math.max(-0.5, Math.min(1.5, this.orbitPitch));
});
window.addEventListener('mouseup', () => {
this.isDragging = false;
});
// Touch support for mobile
window.addEventListener('touchstart', (e) => {
if (e.touches.length > 0) {
this.isDragging = true;
this.lastMouseX = e.touches[0].clientX;
this.lastMouseY = e.touches[0].clientY;
}
});
window.addEventListener('touchmove', (e) => {
if (!this.isDragging || e.touches.length === 0) return;
const dx = e.touches[0].clientX - this.lastMouseX;
const dy = e.touches[0].clientY - this.lastMouseY;
this.lastMouseX = e.touches[0].clientX;
this.lastMouseY = e.touches[0].clientY;
this.orbitYaw += dx * 0.01;
this.orbitPitch += dy * 0.01;
this.orbitPitch = Math.max(-0.5, Math.min(1.5, this.orbitPitch));
});
window.addEventListener('touchend', () => {
this.isDragging = false;
});
}
update(carMesh, dt) {
const carPos = carMesh.position;
const heading = carMesh.rotation.y;
// If not dragging, smoothly return the camera to the default forward-facing angle
if (!this.isDragging) {
this.orbitYaw *= 0.95;
this.orbitPitch *= 0.95;
}
// Apply the free-look orbit rotation
const baseHeading = heading + this.orbitYaw;
// Desired camera position: behind and above the car
const behindDir = new THREE.Vector3(
Math.sin(baseHeading),
0,
Math.cos(baseHeading)
);
// Calculate height offset based on pitch
const currentAbove = OFFSET_ABOVE + this.orbitPitch * 5;
this._tmpPos.copy(carPos)
.addScaledVector(behindDir, -OFFSET_BEHIND)
.setY(carPos.y + currentAbove);
// Lerp camera position
this.camera.position.lerp(this._tmpPos, LERP_POS);
// Look-at target: we look directly at the car, but adjust height for pitch
this._tmpLook.copy(carPos).setY(carPos.y + 1.5 - this.orbitPitch * 2);
// Lerp look target
this.targetLook.lerp(this._tmpLook, LERP_LOOK);
this.camera.lookAt(this.targetLook);
}
snapTo(carMesh) {
const carPos = carMesh.position;
const heading = carMesh.rotation.y;
const behindDir = new THREE.Vector3(Math.sin(heading), 0, Math.cos(heading));
this.camera.position.copy(carPos).addScaledVector(behindDir, OFFSET_BEHIND).setY(carPos.y + OFFSET_ABOVE);
this.targetLook.copy(carPos).addScaledVector(behindDir, -LOOK_AHEAD).setY(carPos.y + 1.0);
this.camera.lookAt(this.targetLook);
}
}