diff --git a/js/Camera.js b/js/Camera.js
index cde2d36..4316d09 100644
--- a/js/Camera.js
+++ b/js/Camera.js
@@ -1,8 +1,45 @@
import * as THREE from 'three';
+export const CAMERA_MODE = {
+ EAGLE: 0,
+ THIRD: 1,
+ HOOD: 2,
+};
+
const _desired = new THREE.Vector3();
const _delta = new THREE.Vector3();
const _lookPoint = new THREE.Vector3();
+const _vehForward = new THREE.Vector3();
+const _vehRight = new THREE.Vector3();
+const _localOffset = new THREE.Vector3();
+const _goalPos = new THREE.Vector3();
+const _roofPos = new THREE.Vector3();
+const _hoodPos = new THREE.Vector3();
+const _thirdGoalPos = new THREE.Vector3();
+const _lookAt = new THREE.Vector3();
+const _tmpQuatA = new THREE.Quaternion();
+const _tmpQuatB = new THREE.Quaternion();
+const _yawQuat = new THREE.Quaternion();
+const _up = new THREE.Vector3( 0, 1, 0 );
+const _camPosSave = new THREE.Vector3();
+const _camQuatSave = new THREE.Quaternion();
+
+function smootherstep( t ) {
+
+ if ( t <= 0 ) return 0;
+ if ( t >= 1 ) return 1;
+ return t * t * t * ( t * ( t * 6 - 15 ) + 10 );
+
+}
+
+function transitionDuration( from, to ) {
+
+ if ( from === CAMERA_MODE.EAGLE && to === CAMERA_MODE.THIRD ) return 0.42;
+ if ( from === CAMERA_MODE.THIRD && to === CAMERA_MODE.HOOD ) return 0.58;
+ if ( from === CAMERA_MODE.HOOD && to === CAMERA_MODE.EAGLE ) return 0.48;
+ return 0.45;
+
+}
export class Camera {
@@ -17,18 +54,49 @@ export class Camera {
this.camera.lookAt( 0, 0, 0 );
// Camera-aligned ground basis (XZ plane), derived from offset.
- // camRightXZ: screen-right projected to ground.
- // camForwardXZ: screen-up (away from camera) projected to ground.
this.camRightXZ = new THREE.Vector3( this.offset.z, 0, - this.offset.x ).normalize();
this.camForwardXZ = new THREE.Vector3( - this.offset.x, 0, - this.offset.z ).normalize();
- this.leadFactor = 3.0;
- this.cameraSmoothing = 2.0;
- this.deadzoneRadius = 5.0;
+ this.leadFactor = 10.0;
+ this.cameraSmoothing = 5.0;
+ this.deadzoneRadius = 2.0;
this.screenShiftUp = 1.0;
this.smoothedDesired = new THREE.Vector3();
- this.initialized = false;
+ this.smoothedThird = new THREE.Vector3();
+ this.eagleInitialized = false;
+ this.thirdInitialized = false;
+
+ // Third-person chase (vehicle-local offset, rotated by yaw)
+ this.thirdBack = 3.0;
+ this.thirdHeight = 1.85;
+ this.thirdScreenShift = .5;
+
+ // Hood / cockpit (local space: +Z forward, +X right, +Y up)
+ this.hoodLocal = new THREE.Vector3( -0.0, 0.1, 0.15 );
+ this.roofApproachLocal = new THREE.Vector3( 1.15, 3.05, 2.35 );
+ this.hoodLookAhead = 14;
+
+ this.steadyMode = CAMERA_MODE.EAGLE;
+ this.transitionTargetMode = CAMERA_MODE.EAGLE;
+ this.transitionEnterSource = CAMERA_MODE.EAGLE;
+ this.transitionActive = false;
+ this.transitionElapsed = 0;
+ this.transitionDuration = 0.45;
+
+ this.startPos = new THREE.Vector3();
+ this.startQuat = new THREE.Quaternion();
+ this.goalQuat = new THREE.Quaternion();
+ this.blendQuat = new THREE.Quaternion();
+
+ this.baseFov = 40;
+ this.closeFovMax = 58;
+ this.smoothedFov = this.baseFov;
+
+ // Hood: barrel roll from steering (same family as Vehicle body rotation.z)
+ this.hoodRollMax = 0.28;
+ this.hoodSteerRollGain = 1.35;
+ this.smoothedHoodRoll = 0;
const segments = 64;
const points = [];
@@ -55,15 +123,129 @@ export class Camera {
}
- update( dt, target, velocity ) {
+ _fovCloseModeWeight() {
+
+ if ( ! this.transitionActive ) {
+
+ return ( this.steadyMode === CAMERA_MODE.THIRD || this.steadyMode === CAMERA_MODE.HOOD ) ? 1 : 0;
+
+ }
+
+ const u = THREE.MathUtils.clamp( this.transitionElapsed / this.transitionDuration, 0, 1 );
+ const e = smootherstep( u );
+
+ if ( this.transitionTargetMode === CAMERA_MODE.EAGLE ) return 1 - e;
+
+ if ( this.transitionTargetMode === CAMERA_MODE.THIRD && this.transitionEnterSource === CAMERA_MODE.EAGLE ) return e;
+
+ return 1;
+
+ }
+
+ _hoodGForceBlend() {
+
+ if ( ! this.transitionActive ) {
+
+ return this.steadyMode === CAMERA_MODE.HOOD ? 1 : 0;
+
+ }
+
+ const u = THREE.MathUtils.clamp( this.transitionElapsed / this.transitionDuration, 0, 1 );
+ const e = smootherstep( u );
+
+ const thirdToHood = this.transitionEnterSource === CAMERA_MODE.THIRD &&
+ this.transitionTargetMode === CAMERA_MODE.HOOD;
+
+ if ( thirdToHood ) {
+
+ const split = 0.48;
+ if ( u <= split ) return 0;
+
+ return smootherstep( ( u - split ) / ( 1 - split ) );
+
+ }
+
+ if ( this.transitionTargetMode === CAMERA_MODE.EAGLE && this.transitionEnterSource === CAMERA_MODE.HOOD ) {
+
+ return 1 - e;
+
+ }
+
+ return 0;
+
+ }
+
+ _applyHoodSteerRoll( dt, vehicle ) {
+
+ const w = this._hoodGForceBlend();
+ // Match bodyNode.rotation.z drive: -(inputX/5)*linearSpeed — tilt against steering side.
+ const steer = vehicle.inputX ?? 0;
+ const speed = vehicle.linearSpeed ?? 0;
+ const raw = - ( steer / 10 ) * speed * this.hoodSteerRollGain;
+ const target = THREE.MathUtils.clamp( raw, - this.hoodRollMax, this.hoodRollMax ) * w;
+ const alpha = 1 - Math.exp( - dt * 2 );
+ this.smoothedHoodRoll += ( target - this.smoothedHoodRoll ) * alpha;
+ this.camera.rotateZ( -this.smoothedHoodRoll );
+
+ }
+
+ _applySpeedFov( dt, speedNorm ) {
+
+ const w = this._fovCloseModeWeight();
+ const s = THREE.MathUtils.clamp( speedNorm, 0, 1 );
+ const se = s * s * ( 3 - 2 * s );
+ const targetFov = THREE.MathUtils.lerp( this.baseFov, this.closeFovMax, w * se );
+ const alpha = 1 - Math.exp( - dt * 14 );
+ this.smoothedFov += ( targetFov - this.smoothedFov ) * alpha;
+ this.camera.fov = this.smoothedFov;
+ this.camera.updateProjectionMatrix();
+
+ }
+
+ advanceMode( spherePos ) {
+
+ const prevDest = this.transitionActive ? this.transitionTargetMode : this.steadyMode;
+ const next = ( prevDest + 1 ) % 3;
+
+ this.transitionEnterSource = prevDest;
+ this.transitionTargetMode = next;
+ this.transitionDuration = transitionDuration( prevDest, next );
+ this.transitionElapsed = 0;
+ this.transitionActive = true;
+ this.startPos.copy( this.camera.position );
+ this.startQuat.copy( this.camera.quaternion );
+
+ if ( next === CAMERA_MODE.THIRD && prevDest === CAMERA_MODE.EAGLE && spherePos ) {
+
+ this.smoothedThird.copy( spherePos );
+ this.thirdInitialized = true;
+
+ }
+
+ if ( next === CAMERA_MODE.EAGLE && prevDest === CAMERA_MODE.HOOD && spherePos ) {
+
+ this.smoothedDesired.copy( spherePos );
+ this.eagleInitialized = true;
+
+ }
+
+ }
+
+ /** Mode index for HUD highlight (0 eagle, 1 third, 2 hood). */
+ getHudMode() {
+
+ return this.transitionActive ? this.transitionTargetMode : this.steadyMode;
+
+ }
+
+ _applyLeadDeadzone( target, velocity, rightXZ, forwardXZ, smoothed, initializedFlag, dt ) {
const radius = this.deadzoneRadius;
const radiusSq = radius * radius;
+ const leadMul = this.transitionActive ? 0.42 : 1;
- // Lead = velocity projected onto camera-aligned ground basis, scaled, clamped to the deadzone disk.
- // Becomes the camera's offset from the car: car settles at the trailing edge of the circle.
- let leadX = velocity.dot( this.camRightXZ ) * this.leadFactor;
- let leadY = velocity.dot( this.camForwardXZ ) * this.leadFactor;
+ let leadX = velocity.dot( rightXZ ) * this.leadFactor * leadMul;
+ let leadY = velocity.dot( forwardXZ ) * this.leadFactor * leadMul;
const leadLenSq = leadX * leadX + leadY * leadY;
if ( leadLenSq > radiusSq ) {
@@ -74,38 +256,212 @@ export class Camera {
}
_desired.copy( target )
- .addScaledVector( this.camRightXZ, leadX )
- .addScaledVector( this.camForwardXZ, leadY );
+ .addScaledVector( rightXZ, leadX )
+ .addScaledVector( forwardXZ, leadY );
- const alpha = this.initialized ? 1 - Math.exp( - dt * this.cameraSmoothing ) : 1;
- this.smoothedDesired.lerp( _desired, alpha );
- this.initialized = true;
+ const alpha = initializedFlag ? 1 - Math.exp( - dt * this.cameraSmoothing ) : 1;
+ smoothed.lerp( _desired, alpha );
- // Hard-clamp: car must not escape the deadzone, even if the lerp lags at high speed.
- _delta.subVectors( target, this.smoothedDesired );
- const offsetX = _delta.dot( this.camRightXZ );
- const offsetY = _delta.dot( this.camForwardXZ );
+ _delta.subVectors( target, smoothed );
+ const offsetX = _delta.dot( rightXZ );
+ const offsetY = _delta.dot( forwardXZ );
const offsetLenSq = offsetX * offsetX + offsetY * offsetY;
if ( offsetLenSq > radiusSq ) {
const offsetLen = Math.sqrt( offsetLenSq );
const k = ( offsetLen - radius ) / offsetLen;
- this.smoothedDesired
- .addScaledVector( this.camRightXZ, offsetX * k )
- .addScaledVector( this.camForwardXZ, offsetY * k );
+ smoothed
+ .addScaledVector( rightXZ, offsetX * k )
+ .addScaledVector( forwardXZ, offsetY * k );
}
- // Shift the entire view (camera + lookAt) so smoothedDesired sits higher on screen.
- _lookPoint.copy( this.smoothedDesired ).addScaledVector( this.camForwardXZ, - this.screenShiftUp );
+ return radius;
+
+ }
- this.camera.position.copy( _lookPoint ).add( this.offset );
- this.camera.lookAt( _lookPoint );
+ _vehicleYawBasis( vehicle ) {
+
+ const g = vehicle.container;
+ _vehForward.set( 0, 0, 1 ).applyQuaternion( g.quaternion );
+ _vehForward.y = 0;
+ const fl = _vehForward.length();
+ if ( fl < 1e-5 ) _vehForward.set( 0, 0, 1 );
+ else _vehForward.multiplyScalar( 1 / fl );
+
+ _vehRight.crossVectors( _up, _vehForward ).normalize();
+
+ const yaw = Math.atan2( _vehForward.x, _vehForward.z );
+ _yawQuat.setFromAxisAngle( _up, yaw );
+
+ }
+
+ _poseFromLookAt( eye, lookAt, outPos, outQuat ) {
+
+ // Must use PerspectiveCamera.lookAt: plain Object3D.lookAt swaps eye/target in three.js,
+ // so a dummy object yields the wrong quaternion for this.camera.
+ const cam = this.camera;
+ _camPosSave.copy( cam.position );
+ _camQuatSave.copy( cam.quaternion );
+ cam.position.copy( eye );
+ cam.lookAt( lookAt );
+ outPos.copy( eye );
+ outQuat.copy( cam.quaternion );
+ cam.position.copy( _camPosSave );
+ cam.quaternion.copy( _camQuatSave );
+
+ }
+
+ _eagleGoal( dt, target, velocity, outPos, outQuat ) {
+
+ const radius = this._applyLeadDeadzone(
+ target, velocity, this.camRightXZ, this.camForwardXZ,
+ this.smoothedDesired, this.eagleInitialized, dt
+ );
+ this.eagleInitialized = true;
+
+ _lookPoint.copy( this.smoothedDesired ).addScaledVector( this.camForwardXZ, - this.screenShiftUp );
+ outPos.copy( _lookPoint ).add( this.offset );
+ this._poseFromLookAt( outPos, _lookPoint, outPos, outQuat );
this.debug.position.copy( this.smoothedDesired );
this.debug.position.y += 0.05;
this.debug.scale.set( radius, 1, radius );
+ return radius;
+
+ }
+
+ _thirdGoal( dt, target, velocity, vehicle, outPos, outQuat ) {
+
+ this._vehicleYawBasis( vehicle );
+
+ const radius = this._applyLeadDeadzone(
+ target, velocity, _vehRight, _vehForward,
+ this.smoothedThird, this.thirdInitialized, dt
+ );
+ this.thirdInitialized = true;
+
+ _lookPoint.copy( this.smoothedThird ).addScaledVector( _vehForward, - this.thirdScreenShift );
+
+ _localOffset.set( 0, this.thirdHeight, - this.thirdBack );
+ _localOffset.applyQuaternion( _yawQuat );
+
+ outPos.copy( _lookPoint ).add( _localOffset );
+ this._poseFromLookAt( outPos, _lookPoint, outPos, outQuat );
+
+ return radius;
+
+ }
+
+ _roofGoal( spherePos, vehicle, outPos, outQuat ) {
+
+ this._vehicleYawBasis( vehicle );
+
+ outPos.copy( this.roofApproachLocal ).applyQuaternion( _yawQuat ).add( spherePos );
+ _lookAt.copy( _vehForward ).multiplyScalar( 10 ).add( outPos );
+ _lookAt.y -= 0.35;
+ this._poseFromLookAt( outPos, _lookAt, outPos, outQuat );
+
+ }
+
+ _hoodGoal( spherePos, vehicle, outPos, outQuat ) {
+
+ this._vehicleYawBasis( vehicle );
+
+ outPos.copy( this.hoodLocal ).applyQuaternion( _yawQuat ).add( spherePos );
+ _lookAt.copy( _vehForward ).multiplyScalar( this.hoodLookAhead ).add( outPos );
+ _lookAt.y -= 0.22;
+ this._poseFromLookAt( outPos, _lookAt, outPos, outQuat );
+
+ }
+
+ _rawModeGoal( mode, dt, spherePos, velocity, vehicle, outPos, outQuat ) {
+
+ switch ( mode ) {
+
+ case CAMERA_MODE.EAGLE:
+ this._eagleGoal( dt, spherePos, velocity, outPos, outQuat );
+ break;
+ case CAMERA_MODE.THIRD:
+ this._thirdGoal( dt, spherePos, velocity, vehicle, outPos, outQuat );
+ break;
+ default:
+ this._hoodGoal( spherePos, vehicle, outPos, outQuat );
+
+ }
+
+ }
+
+ _transitionGoal( dt, spherePos, velocity, vehicle, outPos, outQuat ) {
+
+ const u = this.transitionElapsed / this.transitionDuration;
+
+ const thirdToHood = this.transitionEnterSource === CAMERA_MODE.THIRD &&
+ this.transitionTargetMode === CAMERA_MODE.HOOD;
+
+ if ( thirdToHood ) {
+
+ this._thirdGoal( dt, spherePos, velocity, vehicle, _thirdGoalPos, _tmpQuatA );
+ this._roofGoal( spherePos, vehicle, _roofPos, _tmpQuatB );
+
+ const split = 0.48;
+ let k;
+ if ( u <= split ) {
+
+ k = smootherstep( u / split );
+ outPos.copy( _thirdGoalPos ).lerp( _roofPos, k );
+ this.blendQuat.copy( _tmpQuatA ).slerp( _tmpQuatB, k );
+
+ } else {
+
+ k = smootherstep( ( u - split ) / ( 1 - split ) );
+ this._hoodGoal( spherePos, vehicle, _hoodPos, this.goalQuat );
+ outPos.copy( _roofPos ).lerp( _hoodPos, k );
+ this.blendQuat.copy( _tmpQuatB ).slerp( this.goalQuat, k );
+
+ }
+
+ outQuat.copy( this.blendQuat );
+ return;
+
+ }
+
+ this._rawModeGoal( this.transitionTargetMode, dt, spherePos, velocity, vehicle, outPos, outQuat );
+
+ }
+
+ update( dt, spherePos, velocity, vehicle, speedNorm ) {
+
+ if ( ! this.transitionActive ) {
+
+ this._rawModeGoal( this.steadyMode, dt, spherePos, velocity, vehicle, _goalPos, this.goalQuat );
+ this.camera.position.copy( _goalPos );
+ this.camera.quaternion.copy( this.goalQuat );
+
+ } else {
+
+ this._transitionGoal( dt, spherePos, velocity, vehicle, _goalPos, this.goalQuat );
+
+ const u = smootherstep( this.transitionElapsed / this.transitionDuration );
+ this.camera.position.copy( this.startPos ).lerp( _goalPos, u );
+ this.blendQuat.copy( this.startQuat ).slerp( this.goalQuat, u );
+ this.camera.quaternion.copy( this.blendQuat );
+
+ this.transitionElapsed += dt;
+ if ( this.transitionElapsed >= this.transitionDuration ) {
+
+ this.transitionActive = false;
+ this.steadyMode = this.transitionTargetMode;
+ this.transitionElapsed = this.transitionDuration;
+
+ }
+
+ }
+
+ this._applyHoodSteerRoll( dt, vehicle );
+ this._applySpeedFov( dt, speedNorm ?? 0 );
+
}
}
diff --git a/js/CameraHud.js b/js/CameraHud.js
new file mode 100644
index 0000000..621f5cf
--- /dev/null
+++ b/js/CameraHud.js
@@ -0,0 +1,139 @@
+const LABELS = [ 'Eagle', 'Third person', 'Hood' ];
+
+export class CameraHud {
+
+ constructor( { onSwap } ) {
+
+ this._onSwap = onSwap;
+ this._build();
+ this._syncTop();
+ window.addEventListener( 'resize', () => this._syncTop() );
+
+ }
+
+ _build() {
+
+ const style = document.createElement( 'style' );
+ style.textContent = `
+ #camera-hud {
+ position: absolute;
+ left: 12px;
+ z-index: 10;
+ font: 600 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ user-select: none;
+ min-width: 140px;
+ }
+ #camera-hud .cam-switch {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+ padding: 8px 12px;
+ margin: 0 0 8px 0;
+ border: none;
+ border-radius: 8px;
+ background: #fff;
+ color: #000;
+ font: inherit;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ cursor: pointer;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+ }
+ #camera-hud .cam-switch svg {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ }
+ #camera-hud .cam-switch:hover { filter: brightness(0.96); }
+ #camera-hud .cam-switch:active { transform: scale(0.98); }
+ #camera-hud .cam-list {
+ margin: 0;
+ padding: 10px 12px 10px 1.35em;
+ list-style: disc;
+ line-height: 1.55;
+ background: rgba(0,0,0,0.45);
+ border-radius: 10px;
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ }
+ #camera-hud .cam-list li {
+ margin: 0;
+ padding: 0;
+ }
+ #camera-hud .cam-list li.is-active {
+ color: #fff;
+ -webkit-text-stroke: 0.9px #000;
+ paint-order: stroke fill;
+ }
+ #camera-hud .cam-list li.is-idle {
+ color: rgba(255,255,255,0.38);
+ -webkit-text-stroke: 0;
+ }
+ `;
+ document.head.appendChild( style );
+
+ this.root = document.createElement( 'div' );
+ this.root.id = 'camera-hud';
+
+ const btn = document.createElement( 'button' );
+ btn.type = 'button';
+ btn.className = 'cam-switch';
+ btn.innerHTML =
+ '' +
+ 'Switch (C)';
+ btn.addEventListener( 'click', () => this._onSwap() );
+ this.root.appendChild( btn );
+
+ const ul = document.createElement( 'ul' );
+ ul.className = 'cam-list';
+ this._items = [];
+ for ( let i = 0; i < 3; i ++ ) {
+
+ const li = document.createElement( 'li' );
+ li.textContent = LABELS[ i ];
+ ul.appendChild( li );
+ this._items.push( li );
+
+ }
+
+ this.root.appendChild( ul );
+ document.body.appendChild( this.root );
+
+ }
+
+ _syncTop() {
+
+ const lap = document.getElementById( 'lap-timer' );
+ if ( lap ) {
+
+ const r = lap.getBoundingClientRect();
+ this.root.style.top = `${ Math.round( r.bottom + 8 ) }px`;
+
+ } else {
+
+ this.root.style.top = '12px';
+
+ }
+
+ }
+
+ update( cam ) {
+
+ const mode = cam.getHudMode();
+ for ( let i = 0; i < 3; i ++ ) {
+
+ const li = this._items[ i ];
+ const on = i === mode;
+ li.classList.toggle( 'is-active', on );
+ li.classList.toggle( 'is-idle', ! on );
+
+ }
+
+ }
+
+}
diff --git a/js/Controls.js b/js/Controls.js
index 80b8d09..7c349b6 100644
--- a/js/Controls.js
+++ b/js/Controls.js
@@ -14,6 +14,8 @@ export class Controls {
this.steerStartX = 0;
this.steerStartY = 0;
+ this._keyCDown = false;
+
window.addEventListener( 'keydown', ( e ) => this.keys[ e.code ] = true );
window.addEventListener( 'keyup', ( e ) => this.keys[ e.code ] = false );
@@ -156,7 +158,11 @@ export class Controls {
this.x = x;
this.z = z;
- return { x, z, touchActive: this.touchActive };
+ const cDown = !! this.keys[ 'KeyC' ];
+ const cycleCamera = cDown && ! this._keyCDown;
+ this._keyCDown = cDown;
+
+ return { x, z, touchActive: this.touchActive, cycleCamera };
}
diff --git a/js/Vehicle.js b/js/Vehicle.js
index 97eda6a..8c11555 100644
--- a/js/Vehicle.js
+++ b/js/Vehicle.js
@@ -53,6 +53,10 @@ export class Vehicle {
this.driftIntensity = 0;
+ /** World-space lateral acceleration along car right (XZ), for cockpit camera roll. */
+ this.lateralAccel = 0;
+ this._prevVLateral = 0;
+
}
init( model ) {
@@ -181,6 +185,24 @@ export class Vehicle {
const vel = this.rigidBody.motionProperties.linearVelocity;
this.sphereVel.set( vel[ 0 ], vel[ 1 ], vel[ 2 ] );
+ const vLat = this.sphereVel.dot( _right );
+ if ( dt > 1e-6 && dt < 0.2 ) {
+
+ this.lateralAccel = ( vLat - this._prevVLateral ) / dt;
+
+ } else {
+
+ this.lateralAccel = 0;
+
+ }
+
+ this._prevVLateral = vLat;
+
+ } else {
+
+ this.lateralAccel = 0;
+ this._prevVLateral = 0;
+
}
this.acceleration = THREE.MathUtils.lerp(
@@ -204,6 +226,8 @@ export class Vehicle {
this.linearSpeed = 0;
this.angularSpeed = 0;
this.acceleration = 0;
+ this.lateralAccel = 0;
+ this._prevVLateral = 0;
this.container.rotation.set( 0, 0, 0 );
this.container.quaternion.identity();
diff --git a/js/main.js b/js/main.js
index d87126a..f2cbe0b 100644
--- a/js/main.js
+++ b/js/main.js
@@ -12,6 +12,7 @@ import { SmokeTrails } from './Particles.js';
import { DriftMarks } from './DriftMarks.js';
import { GameAudio } from './Audio.js';
import { LapTimer } from './LapTimer.js';
+import { CameraHud } from './CameraHud.js';
import { ColorMapGLTFLoader } from './Loader.js';
@@ -233,6 +234,10 @@ async function init() {
const lapTimer = new LapTimer( customCells, mapParam );
+ const cameraHud = new CameraHud( {
+ onSwap: () => cam.advanceMode( vehicle.spherePos ),
+ } );
+
const _forward = new THREE.Vector3();
const _camLead = new THREE.Vector3();
@@ -266,6 +271,8 @@ async function init() {
vehicle.update( dt, input );
+ if ( input.cycleCamera ) cam.advanceMode( vehicle.spherePos );
+
dirLight.position.set(
vehicle.spherePos.x + 11.4,
15,
@@ -274,7 +281,8 @@ async function init() {
const mv = vehicle.modelVelocity;
_camLead.set( 0, 0, 1 ).applyQuaternion( vehicle.container.quaternion ).multiplyScalar( Math.sqrt( mv.x * mv.x + mv.z * mv.z ) );
- cam.update( dt, vehicle.spherePos, _camLead );
+ const speedNorm = Math.min( Math.abs( vehicle.linearSpeed ) / MAX_SPEED, 1 );
+ cam.update( dt, vehicle.spherePos, _camLead, vehicle, speedNorm );
particles.update( dt, vehicle );
driftMarks.update( dt, vehicle );
audio.update( dt, vehicle.linearSpeed / MAX_SPEED, input.z, vehicle.driftIntensity );
@@ -282,6 +290,8 @@ async function init() {
const hasInput = input.touchActive || Math.abs( input.x ) > 0.05 || Math.abs( input.z ) > 0.05;
lapTimer.update( dt, vehicle.spherePos, hasInput );
+ cameraHud.update( cam );
+
renderer.render( scene, cam.camera );
}