Feat: lebendigere Avatar-Bewegung — Blickkontakt (lookAt), Umschauen, Gewichtsverlagerung, Vorlehnen beim Sprechen

Avatar schaut zur Kamera (vrm.lookAt = camera), Kopf driftet zu wechselnden Zielen (Umschauen),
Hüfte/Arme verlagern Gewicht, lehnt sich beim Sprechen leicht vor. Über Ruhepose + Lippensync/Mimik.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-28 02:28:00 +02:00
parent 15d5598eec
commit b4a8f92cfa
4 changed files with 79 additions and 57 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-CS3v-0XV.js"></script> <script type="module" crossorigin src="/assets/index-JmrFoH9g.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-jNLzkOWh.css"> <link rel="stylesheet" crossorigin href="/assets/index-jNLzkOWh.css">
</head> </head>
<body> <body>
+41 -19
View File
@@ -29,29 +29,33 @@ function applyRestPose(vrm: VRM) {
vrm.humanoid?.update() vrm.humanoid?.update()
} }
// Lebendige Idle-Animation: prozedural (kein Animations-File). Atmung, langsames Wiegen, // Lebendige Idle-Animation: prozedural (kein Animations-File). Über die Ruhepose gelegt:
// Kopf-/Blickbewegung und Arm-Pendeln werden überlagert; beim Sprechen (level) nickt der Kopf // Atmung, langsames Wiegen + Gewichtsverlagerung, Umschauen (lookYaw/Pitch), Vorlehnen beim
// stärker, damit der Avatar „mitgeht". Wird jede Frame über die Ruhepose gelegt. // Sprechen (lean) und Arm-Mitbewegung. Die Augen (lookAt) hält die useFrame separat auf die Kamera.
function applyIdle(vrm: VRM, t: number, level: number) { function applyIdle(vrm: VRM, t: number, level: number, lookYaw: number, lookPitch: number, lean: number) {
const breathe = Math.sin(t * 1.7) // ~0.27 Hz Atmung const breathe = Math.sin(t * 1.7) // ~0.27 Hz Atmung
const sway = Math.sin(t * 0.5) const sway = Math.sin(t * 0.45) // sanftes Wiegen
const weight = Math.sin(t * 0.32) // langsame Gewichtsverlagerung
const emphasis = Math.min(1, level * 1.4) const emphasis = Math.min(1, level * 1.4)
const nod = Math.sin(t * 1.3) * emphasis * 0.06 // Sprech-Nicken
// Rumpf — Atmung + sanftes Wiegen // Rumpf — Atmung, Wiegen, Gewichtsverlagerung, Vorlehnen beim Sprechen
setBone(vrm, "spine", breathe * 0.025, sway * 0.03, Math.sin(t * 0.43) * 0.015) setBone(vrm, "hips", 0, weight * 0.045, weight * 0.03)
setBone(vrm, "chest", breathe * 0.02, sway * 0.02, 0) setBone(vrm, "spine", breathe * 0.025 + lean * 0.07, sway * 0.022, -weight * 0.03)
setBone(vrm, "hips", 0, Math.sin(t * 0.37) * 0.02, Math.sin(t * 0.29) * 0.01) setBone(vrm, "chest", breathe * 0.02 + lean * 0.02, sway * 0.018, 0)
setBone(vrm, "upperChest", breathe * 0.015, 0, 0)
// Kopf/Hals — langsamer Blick + Atmung + Sprech-Nicken // Kopf/Hals — Umschauen (gedriftete Zielwinkel) + Atmung + Sprech-Nicken
setBone(vrm, "neck", Math.sin(t * 0.8) * 0.03 + emphasis * 0.06, Math.sin(t * 0.31) * 0.05, 0) setBone(vrm, "neck", lookPitch * 0.4 + nod * 0.5, lookYaw * 0.4, 0)
setBone(vrm, "head", Math.sin(t * 0.6) * 0.025 + emphasis * 0.05, Math.sin(t * 0.27) * 0.06, Math.sin(t * 0.5) * 0.02) setBone(vrm, "head", lookPitch * 0.6 + nod * 0.5 + Math.sin(t * 0.6) * 0.015,
lookYaw * 0.6 + Math.sin(t * 0.27) * 0.025, Math.sin(t * 0.5) * 0.02)
// Arme — Ruhepose + leichtes Pendeln // Arme — Ruhepose + leichtes Pendeln + Gewichtsverlagerung
const arm = Math.sin(t * 0.8) * 0.04 const arm = Math.sin(t * 0.8) * 0.035
setBone(vrm, "leftUpperArm", 0, 0, 1.2 + arm) setBone(vrm, "leftUpperArm", 0, 0, 1.18 + arm + weight * 0.04)
setBone(vrm, "rightUpperArm", 0, 0, -1.2 - arm) setBone(vrm, "rightUpperArm", 0, 0, -1.18 - arm + weight * 0.04)
setBone(vrm, "leftLowerArm", 0, -0.2 - Math.sin(t * 0.8) * 0.03, 0) setBone(vrm, "leftLowerArm", 0, -0.18 - Math.sin(t * 0.8) * 0.03, 0)
setBone(vrm, "rightLowerArm", 0, 0.2 + Math.sin(t * 0.8) * 0.03, 0) setBone(vrm, "rightLowerArm", 0, 0.18 + Math.sin(t * 0.8) * 0.03, 0)
} }
const EXPRESSIONS = ["happy", "angry", "sad", "surprised", "relaxed"] as const const EXPRESSIONS = ["happy", "angry", "sad", "surprised", "relaxed"] as const
@@ -65,6 +69,8 @@ function VrmModel({ url, audioLevel, emotion, onError }: {
const [vrm, setVrm] = useState<VRM | null>(null) const [vrm, setVrm] = useState<VRM | null>(null)
const smooth = useRef<Record<string, number>>({}) const smooth = useRef<Record<string, number>>({})
const blink = useRef({ t: 0, next: 3, active: 0 }) const blink = useRef({ t: 0, next: 3, active: 0 })
// Umschauen (gedriftete Kopf-Zielwinkel) + geglättetes Lehnen beim Sprechen.
const motion = useRef({ yaw: 0, pitch: 0, tYaw: 0, tPitch: 0, t: 0, next: 2.5, lean: 0 })
useEffect(() => { useEffect(() => {
let disposed = false let disposed = false
@@ -98,8 +104,24 @@ function VrmModel({ url, audioLevel, emotion, onError }: {
if (!vrm) return if (!vrm) return
const target = audioLevel.current?.current ?? 0 const target = audioLevel.current?.current ?? 0
// Blickkontakt: Augen folgen der Kamera (schaut dich an).
if (vrm.lookAt) vrm.lookAt.target = state.camera
// Umschauen: alle paar Sekunden neues Kopf-Ziel, sanft hineinlerpen.
const m = motion.current
m.t += delta
if (m.t > m.next) {
m.tYaw = (Math.random() - 0.5) * 0.5 // ±0.25 rad Gieren
m.tPitch = (Math.random() - 0.5) * 0.24
m.t = 0
m.next = 2.5 + Math.random() * 3.5
}
m.yaw += (m.tYaw - m.yaw) * Math.min(1, delta * 1.5)
m.pitch += (m.tPitch - m.pitch) * Math.min(1, delta * 1.5)
m.lean += (Math.min(1, target * 1.6) - m.lean) * Math.min(1, delta * 3)
// Lebendige Idle-/Sprech-Bewegung (über die Ruhepose gelegt). // Lebendige Idle-/Sprech-Bewegung (über die Ruhepose gelegt).
applyIdle(vrm, state.clock.elapsedTime, target) applyIdle(vrm, state.clock.elapsedTime, target, m.yaw, m.pitch, m.lean)
const em = vrm.expressionManager const em = vrm.expressionManager
if (em) { if (em) {