puppet face cam when idle

This commit is contained in:
2026-08-29 20:01:44 +08:00
parent 7be507bff2
commit fbb7388028
2 changed files with 51 additions and 12 deletions
@@ -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,
@@ -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<THREE.BoxGeometry, THREE.Material | THREE.Material[]>;
@@ -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(