pages235/renderer/viewer/three/cameraShake.ts

120 lines
4.2 KiB
TypeScript

import * as THREE from 'three'
import { WorldRendererThree } from './worldrendererThree'
export class CameraShake {
private rollAngle = 0
private get damageRollAmount () { return 5 }
private get damageAnimDuration () { return 200 }
private rollAnimation?: { startTime: number, startRoll: number, targetRoll: number, duration: number, returnToZero?: boolean }
private basePitch = 0
private baseYaw = 0
constructor (public worldRenderer: WorldRendererThree, public onRenderCallbacks: Array<() => void>) {
onRenderCallbacks.push(() => {
this.update()
})
}
setBaseRotation (pitch: number, yaw: number) {
this.basePitch = pitch
this.baseYaw = yaw
this.update()
}
getBaseRotation () {
return { pitch: this.basePitch, yaw: this.baseYaw }
}
shakeFromDamage (yaw?: number) {
// Add roll animation
const startRoll = this.rollAngle
const targetRoll = startRoll + (yaw ?? (Math.random() < 0.5 ? -1 : 1)) * this.damageRollAmount
this.rollAnimation = {
startTime: performance.now(),
startRoll,
targetRoll,
duration: this.damageAnimDuration / 2
}
}
update () {
if (this.worldRenderer.playerStateUtils.isSpectatingEntity()) {
// Remove any shaking when spectating
this.rollAngle = 0
this.rollAnimation = undefined
}
// Update roll animation
if (this.rollAnimation) {
const now = performance.now()
const elapsed = now - this.rollAnimation.startTime
const progress = Math.min(elapsed / this.rollAnimation.duration, 1)
if (this.rollAnimation.returnToZero) {
// Ease back to zero
this.rollAngle = this.rollAnimation.startRoll * (1 - this.easeInOut(progress))
if (progress === 1) {
this.rollAnimation = undefined
}
} else {
// Initial roll
this.rollAngle = this.rollAnimation.startRoll + (this.rollAnimation.targetRoll - this.rollAnimation.startRoll) * this.easeOut(progress)
if (progress === 1) {
// Start return to zero animation
this.rollAnimation = {
startTime: now,
startRoll: this.rollAngle,
targetRoll: 0,
duration: this.damageAnimDuration / 2,
returnToZero: true
}
}
}
}
const camera = this.worldRenderer.cameraObject
if (this.worldRenderer.cameraGroupVr) {
// For VR camera, only apply yaw rotation
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw)
camera.setRotationFromQuaternion(yawQuat)
} else {
// For regular camera, apply all rotations
// Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees)
const pitchOffset = this.addAntiZfightingOffset(this.basePitch)
const yawOffset = this.addAntiZfightingOffset(this.baseYaw)
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset)
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset)
const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
// Combine rotations in the correct order: pitch -> yaw -> roll
const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
camera.setRotationFromQuaternion(finalQuat)
}
}
private easeOut (t: number): number {
return 1 - (1 - t) * (1 - t)
}
private easeInOut (t: number): number {
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
}
private addAntiZfightingOffset (angle: number): number {
const offset = 0.001 // Very small offset in radians (about 0.057 degrees)
// Check if the angle is close to ideal angles (0, π/2, π, 3π/2)
const normalizedAngle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)
const tolerance = 0.01 // Tolerance for considering an angle "ideal"
if (Math.abs(normalizedAngle) < tolerance ||
Math.abs(normalizedAngle - Math.PI / 2) < tolerance ||
Math.abs(normalizedAngle - Math.PI) < tolerance ||
Math.abs(normalizedAngle - 3 * Math.PI / 2) < tolerance) {
return angle + offset
}
return angle
}
}