From fbb7388028927c9c82ef95c983defabf44258be7 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sat, 29 Aug 2026 20:01:44 +0800 Subject: [PATCH] puppet face cam when idle --- .../components/renderer/puppet/animation.ts | 60 +++++++++++++++---- .../scene/components/renderer/puppet/index.ts | 3 + 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/routes/scene/components/renderer/puppet/animation.ts b/src/routes/scene/components/renderer/puppet/animation.ts index dbf9228..2f29292 100644 --- a/src/routes/scene/components/renderer/puppet/animation.ts +++ b/src/routes/scene/components/renderer/puppet/animation.ts @@ -1,12 +1,21 @@ import * as THREE from "three"; import type { PuppetRig } from "."; +const MIN_IDLE_FACING_OFFSET_DEGREES = 3; +const MAX_IDLE_FACING_OFFSET_DEGREES = 5; + export class PuppetAnimation { private readonly smoothedGroundPosition = new THREE.Vector3(); private readonly movementDelta = new THREE.Vector3(); + private readonly idleRotationY: number; + private idleTargetRotationY: number; private hasSmoothedPosition = false; + private wasMoving = false; - constructor(private readonly rig: PuppetRig) {} + constructor(private readonly rig: PuppetRig) { + this.idleRotationY = rig.root.rotation.y; + this.idleTargetRotationY = this.idleRotationY; + } update( targetGroundPosition: THREE.Vector3, @@ -14,6 +23,8 @@ export class PuppetAnimation { deltaSeconds: number, elapsedSeconds: number, ) { + this.updateIdleTarget(isMoving); + if (!this.hasSmoothedPosition) { this.smoothedGroundPosition.copy(targetGroundPosition); this.rig.root.position.copy(targetGroundPosition); @@ -27,6 +38,9 @@ export class PuppetAnimation { if (this.movementDelta.lengthSq() <= 0.01) { this.pause(); this.rig.root.position.copy(targetGroundPosition); + if (!isMoving) { + this.turnTowards(this.idleTargetRotationY, deltaSeconds); + } return; } @@ -34,20 +48,14 @@ export class PuppetAnimation { this.smoothedGroundPosition.lerp(targetGroundPosition, alpha); this.rig.root.position.copy(this.smoothedGroundPosition); - const targetRotationY = Math.atan2( - this.movementDelta.x, - this.movementDelta.z, - ); - this.rig.root.rotation.y = this.dampAngle( - this.rig.root.rotation.y, - targetRotationY, - 14, - deltaSeconds, - ); - if (isMoving) { + this.turnTowards( + Math.atan2(this.movementDelta.x, this.movementDelta.z), + deltaSeconds, + ); this.animateWalkCycle(elapsedSeconds); } else { + this.turnTowards(this.idleTargetRotationY, deltaSeconds); this.pause(); } } @@ -59,6 +67,34 @@ export class PuppetAnimation { this.rig.rightArm.rotation.x *= 0.8; } + private updateIdleTarget(isMoving: boolean) { + if (isMoving) { + this.wasMoving = true; + return; + } + if (!this.wasMoving) return; + + const magnitudeDegrees = THREE.MathUtils.lerp( + MIN_IDLE_FACING_OFFSET_DEGREES, + MAX_IDLE_FACING_OFFSET_DEGREES, + Math.random(), + ); + const direction = Math.random() < 0.5 ? -1 : 1; + this.idleTargetRotationY = + this.idleRotationY + + THREE.MathUtils.degToRad(magnitudeDegrees * direction); + this.wasMoving = false; + } + + private turnTowards(targetRotationY: number, deltaSeconds: number) { + this.rig.root.rotation.y = this.dampAngle( + this.rig.root.rotation.y, + targetRotationY, + 14, + deltaSeconds, + ); + } + private dampAngle( currentRadians: number, targetRadians: number, diff --git a/src/routes/scene/components/renderer/puppet/index.ts b/src/routes/scene/components/renderer/puppet/index.ts index 818986f..51cfccd 100644 --- a/src/routes/scene/components/renderer/puppet/index.ts +++ b/src/routes/scene/components/renderer/puppet/index.ts @@ -4,6 +4,8 @@ import type { World } from "../world"; import { PuppetAnimation } from "./animation"; import { PuppetSkin } from "./skin"; +const CAMERA_FACING_ROTATION_Y = Math.PI / 4; + export type PuppetRig = { root: THREE.Group; body: THREE.Mesh; @@ -89,6 +91,7 @@ export class Puppet { private createRig(): PuppetRig { const root = new THREE.Group(); + root.rotation.y = CAMERA_FACING_ROTATION_Y; const material = new THREE.MeshStandardMaterial(); const body = new THREE.Mesh(